commit
1300773f3d
|
@ -6,6 +6,7 @@ before_install:
|
||||||
- sudo apt-get install -qq mpg123 portaudio19-dev libglib2.0-dev swig bison libtool autoconf libglib2.0-dev libicu-dev libfann-dev realpath
|
- sudo apt-get install -qq mpg123 portaudio19-dev libglib2.0-dev swig bison libtool autoconf libglib2.0-dev libicu-dev libfann-dev realpath
|
||||||
- sudo apt-get install -y gcc-4.8 g++-4.8
|
- sudo apt-get install -y gcc-4.8 g++-4.8
|
||||||
- export CC="gcc-4.8"
|
- export CC="gcc-4.8"
|
||||||
|
- export TMPDIR="/tmp/${TRAVIS_PYTHON_VERSION}"
|
||||||
python:
|
python:
|
||||||
- "3.4"
|
- "3.4"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
|
@ -15,6 +16,9 @@ python:
|
||||||
cache: pocketsphinx-python
|
cache: pocketsphinx-python
|
||||||
# command to install dependencies
|
# command to install dependencies
|
||||||
install:
|
install:
|
||||||
|
- rm -rf ${TMPDIR}
|
||||||
|
- mkdir ${TMPDIR}
|
||||||
|
- echo ${TMPDIR}
|
||||||
- VIRTUALENV_ROOT=${VIRTUAL_ENV} ./dev_setup.sh
|
- VIRTUALENV_ROOT=${VIRTUAL_ENV} ./dev_setup.sh
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install -r test-requirements.txt
|
- pip install -r test-requirements.txt
|
||||||
|
|
|
@ -371,6 +371,11 @@ class DeviceApi(Api):
|
||||||
skills = {s['name']: s for s in data['skills']}
|
skills = {s['name']: s for s in data['skills']}
|
||||||
to_send['skills'] = [skills[key] for key in skills]
|
to_send['skills'] = [skills[key] for key in skills]
|
||||||
|
|
||||||
|
# Finalize skill_gid with uuid if needed
|
||||||
|
for s in to_send['skills']:
|
||||||
|
s['skill_gid'] = s.get('skill_gid', '').replace(
|
||||||
|
'@|', '@{}|'.format(self.identity.uuid))
|
||||||
|
|
||||||
self.request({
|
self.request({
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"path": "/" + self.identity.uuid + "/skillJson",
|
"path": "/" + self.identity.uuid + "/skillJson",
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Copyright 2019 Mycroft AI Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
from os.path import join, expanduser, exists
|
||||||
|
|
||||||
|
from msm import MycroftSkillsManager, SkillRepo
|
||||||
|
from mycroft.util.combo_lock import ComboLock
|
||||||
|
|
||||||
|
mycroft_msm_lock = ComboLock('/tmp/mycroft-msm.lck')
|
||||||
|
|
||||||
|
|
||||||
|
def create_msm(config):
|
||||||
|
""" Create msm object from config. """
|
||||||
|
msm_config = config['skills']['msm']
|
||||||
|
repo_config = msm_config['repo']
|
||||||
|
data_dir = expanduser(config['data_dir'])
|
||||||
|
skills_dir = join(data_dir, msm_config['directory'])
|
||||||
|
repo_cache = join(data_dir, repo_config['cache'])
|
||||||
|
platform = config['enclosure'].get('platform', 'default')
|
||||||
|
|
||||||
|
with mycroft_msm_lock:
|
||||||
|
# Try to create the skills directory if it doesn't exist
|
||||||
|
if not exists(skills_dir):
|
||||||
|
os.makedirs(skills_dir)
|
||||||
|
|
||||||
|
return MycroftSkillsManager(
|
||||||
|
platform=platform, skills_dir=skills_dir,
|
||||||
|
repo=SkillRepo(repo_cache, repo_config['url'],
|
||||||
|
repo_config['branch']),
|
||||||
|
versioned=msm_config['versioned'])
|
|
@ -63,14 +63,52 @@ import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
import time
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
from threading import Timer
|
from threading import Timer
|
||||||
from os.path import isfile, join, expanduser
|
from os.path import isfile, join, expanduser
|
||||||
from requests.exceptions import RequestException, HTTPError
|
from requests.exceptions import RequestException, HTTPError
|
||||||
|
from msm import SkillEntry
|
||||||
|
|
||||||
from mycroft.api import DeviceApi, is_paired
|
from mycroft.api import DeviceApi, is_paired
|
||||||
from mycroft.util.log import LOG
|
from mycroft.util.log import LOG
|
||||||
|
from mycroft.util import camel_case_split
|
||||||
from mycroft.configuration import ConfigurationManager
|
from mycroft.configuration import ConfigurationManager
|
||||||
|
|
||||||
|
from .msm_wrapper import create_msm
|
||||||
|
|
||||||
|
|
||||||
|
msm = None
|
||||||
|
msm_creation_time = 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_global_id(directory, config):
|
||||||
|
""" Create global id for the skill.
|
||||||
|
|
||||||
|
TODO: Handle dirty skill
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
directory: skill directory
|
||||||
|
config: config for the device to fetch msm setup
|
||||||
|
"""
|
||||||
|
# Update the msm object if it's more than an hour old
|
||||||
|
global msm
|
||||||
|
global msm_creation_time
|
||||||
|
if msm is None or time.time() - msm_creation_time > 60 * 60:
|
||||||
|
msm_creation_time = time.time()
|
||||||
|
msm = create_msm(config)
|
||||||
|
|
||||||
|
s = SkillEntry.from_folder(directory, msm)
|
||||||
|
# If modified prepend the device uuid
|
||||||
|
return s.skill_gid, s.meta_info.get('display_name')
|
||||||
|
|
||||||
|
|
||||||
|
def display_name(name):
|
||||||
|
""" Splits camelcase and removes leading/trailing Skill. """
|
||||||
|
name = re.sub(r'(^[Ss]kill|[Ss]kill$)', '', name)
|
||||||
|
return camel_case_split(name)
|
||||||
|
|
||||||
|
|
||||||
class DelayRequest(Exception):
|
class DelayRequest(Exception):
|
||||||
""" Indicate that the next request should be delayed. """
|
""" Indicate that the next request should be delayed. """
|
||||||
|
@ -82,8 +120,10 @@ class SkillSettings(dict):
|
||||||
also syncs to the backend for skill settings
|
also syncs to the backend for skill settings
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
directory (str): Path to storage directory
|
directory (str): Path to storage directory
|
||||||
name (str): user readable name associated with the settings
|
name (str): user readable name associated with the settings
|
||||||
|
no_upload (bool): True if the upload to mycroft servers should be
|
||||||
|
disabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, directory, name):
|
def __init__(self, directory, name):
|
||||||
|
@ -100,6 +140,8 @@ class SkillSettings(dict):
|
||||||
# set file paths
|
# set file paths
|
||||||
self._settings_path = join(directory, 'settings.json')
|
self._settings_path = join(directory, 'settings.json')
|
||||||
self._meta_path = _get_meta_path(directory)
|
self._meta_path = _get_meta_path(directory)
|
||||||
|
self._directory = directory
|
||||||
|
|
||||||
self.is_alive = True
|
self.is_alive = True
|
||||||
self.loaded_hash = hash(json.dumps(self, sort_keys=True))
|
self.loaded_hash = hash(json.dumps(self, sort_keys=True))
|
||||||
self._complete_intialization = False
|
self._complete_intialization = False
|
||||||
|
@ -108,11 +150,33 @@ class SkillSettings(dict):
|
||||||
self._user_identity = None
|
self._user_identity = None
|
||||||
self.changed_callback = None
|
self.changed_callback = None
|
||||||
self._poll_timer = None
|
self._poll_timer = None
|
||||||
|
self._blank_poll_timer = None
|
||||||
self._is_alive = True
|
self._is_alive = True
|
||||||
|
self._meta_upload = True # Flag allowing upload of settings meta
|
||||||
|
|
||||||
|
# Add Information extracted from the skills-meta.json entry for the
|
||||||
|
# skill.
|
||||||
|
skill_gid, disp_name = build_global_id(self._directory, self.config)
|
||||||
|
self.__skill_gid = skill_gid
|
||||||
|
self.display_name = disp_name
|
||||||
|
|
||||||
# if settingsmeta exist
|
# if settingsmeta exist
|
||||||
if self._meta_path:
|
if self._meta_path:
|
||||||
self._poll_skill_settings()
|
self._poll_skill_settings()
|
||||||
|
# if not disallowed by user upload an entry for all skills installed
|
||||||
|
elif self.config['skills']['upload_skill_manifest']:
|
||||||
|
self._blank_poll_timer = Timer(1, self._init_blank_meta)
|
||||||
|
self._blank_poll_timer.daemon = True
|
||||||
|
self._blank_poll_timer.start()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skill_gid(self):
|
||||||
|
""" Finalizes the skill gid to include device uuid if needed. """
|
||||||
|
if is_paired():
|
||||||
|
return self.__skill_gid.replace('@|', '@{}|'.format(
|
||||||
|
DeviceApi().identity.uuid))
|
||||||
|
else:
|
||||||
|
return self.__skill_gid
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
""" Simple object unique hash. """
|
""" Simple object unique hash. """
|
||||||
|
@ -128,6 +192,8 @@ class SkillSettings(dict):
|
||||||
self._is_alive = False
|
self._is_alive = False
|
||||||
if self._poll_timer:
|
if self._poll_timer:
|
||||||
self._poll_timer.cancel()
|
self._poll_timer.cancel()
|
||||||
|
if self._blank_poll_timer:
|
||||||
|
self._blank_poll_timer.cancel()
|
||||||
|
|
||||||
def set_changed_callback(self, callback):
|
def set_changed_callback(self, callback):
|
||||||
""" Set callback to perform when server settings have changed.
|
""" Set callback to perform when server settings have changed.
|
||||||
|
@ -158,34 +224,17 @@ class SkillSettings(dict):
|
||||||
except RequestException:
|
except RequestException:
|
||||||
return
|
return
|
||||||
|
|
||||||
hashed_meta = self._get_meta_hash(settings_meta)
|
settings = (self._request_my_settings(self.skill_gid) or
|
||||||
skill_settings = self._request_other_settings(hashed_meta)
|
self._request_other_settings(self.skill_gid))
|
||||||
# if hash is new then there is a diff version of settingsmeta
|
if settings:
|
||||||
if self._is_new_hash(hashed_meta):
|
self.save_skill_settings(settings)
|
||||||
# first look at all other devices on user account to see
|
|
||||||
# if the settings exist. if it does then sync with device
|
# TODO if this skill_gid is not a modified version check if a modified
|
||||||
if skill_settings:
|
# version exists on the server and delete it
|
||||||
# not_owner flags that this settings is loaded from
|
|
||||||
# another device. If a skill settings doesn't have
|
# Always try to upload settingsmeta on startup
|
||||||
# not_owner, then the skill is created from that device
|
self._upload_meta(settings_meta, self.skill_gid)
|
||||||
self['not_owner'] = True
|
|
||||||
self.save_skill_settings(skill_settings)
|
|
||||||
else: # upload skill settings if
|
|
||||||
uuid = self._load_uuid()
|
|
||||||
if uuid is not None:
|
|
||||||
self._delete_metadata(uuid)
|
|
||||||
self._upload_meta(settings_meta, hashed_meta)
|
|
||||||
else: # hash is not new
|
|
||||||
if skill_settings is not None:
|
|
||||||
self['not_owner'] = True
|
|
||||||
self.save_skill_settings(skill_settings)
|
|
||||||
else:
|
|
||||||
settings = self._request_my_settings(hashed_meta)
|
|
||||||
if settings is None:
|
|
||||||
# metadata got deleted from Home, send up
|
|
||||||
self._upload_meta(settings_meta, hashed_meta)
|
|
||||||
else:
|
|
||||||
self.save_skill_settings(settings)
|
|
||||||
self._complete_intialization = True
|
self._complete_intialization = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -202,24 +251,41 @@ class SkillSettings(dict):
|
||||||
return super(SkillSettings, self).__setitem__(key, value)
|
return super(SkillSettings, self).__setitem__(key, value)
|
||||||
|
|
||||||
def _load_settings_meta(self):
|
def _load_settings_meta(self):
|
||||||
""" Loads settings metadata from skills path. """
|
""" Load settings metadata from the skill folder.
|
||||||
if not self._meta_path:
|
|
||||||
return None
|
|
||||||
|
|
||||||
_, ext = os.path.splitext(self._meta_path)
|
If no settingsmeta exists a basic settingsmeta will be created
|
||||||
json_file = True if ext.lower() == ".json" else False
|
containing a basic identifier.
|
||||||
|
|
||||||
try:
|
Returns:
|
||||||
with open(self._meta_path, encoding='utf-8') as f:
|
(dict) settings meta
|
||||||
if json_file:
|
"""
|
||||||
data = json.load(f)
|
if self._meta_path and os.path.isfile(self._meta_path):
|
||||||
else:
|
_, ext = os.path.splitext(self._meta_path)
|
||||||
data = yaml.load(f)
|
json_file = True if ext.lower() == ".json" else False
|
||||||
return data
|
|
||||||
except Exception as e:
|
try:
|
||||||
LOG.error("Failed to load setting file: " + self._meta_path)
|
with open(self._meta_path, encoding='utf-8') as f:
|
||||||
LOG.error(repr(e))
|
if json_file:
|
||||||
return None
|
data = json.load(f)
|
||||||
|
else:
|
||||||
|
data = yaml.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error("Failed to load setting file: " + self._meta_path)
|
||||||
|
LOG.error(repr(e))
|
||||||
|
data = copy.copy(BLANK_META)
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Insert skill_gid and display_name
|
||||||
|
data['skill_gid'] = self.skill_gid
|
||||||
|
data['display_name'] = (self.display_name or data.get('name') or
|
||||||
|
display_name(self.name))
|
||||||
|
|
||||||
|
# Backwards compatibility:
|
||||||
|
if 'name' not in data:
|
||||||
|
data['name'] = data['display_name']
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def _send_settings_meta(self, settings_meta):
|
def _send_settings_meta(self, settings_meta):
|
||||||
""" Send settingsmeta to the server.
|
""" Send settingsmeta to the server.
|
||||||
|
@ -229,18 +295,21 @@ class SkillSettings(dict):
|
||||||
Returns:
|
Returns:
|
||||||
dict: uuid, a unique id for the setting meta data
|
dict: uuid, a unique id for the setting meta data
|
||||||
"""
|
"""
|
||||||
try:
|
if self._meta_upload:
|
||||||
uuid = self._put_metadata(settings_meta)
|
try:
|
||||||
return uuid
|
uuid = self._put_metadata(settings_meta)
|
||||||
except HTTPError as e:
|
return uuid
|
||||||
if e.response.status_code in [422, 500, 501]:
|
except HTTPError as e:
|
||||||
raise DelayRequest
|
if e.response.status_code in [422, 500, 501]:
|
||||||
else:
|
self._meta_upload = False
|
||||||
|
raise DelayRequest
|
||||||
|
else:
|
||||||
|
LOG.error(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
LOG.error(e)
|
LOG.error(e)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
|
||||||
LOG.error(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def save_skill_settings(self, skill_settings):
|
def save_skill_settings(self, skill_settings):
|
||||||
""" Takes skill object and save onto self
|
""" Takes skill object and save onto self
|
||||||
|
@ -248,61 +317,22 @@ class SkillSettings(dict):
|
||||||
Args:
|
Args:
|
||||||
skill_settings (dict): skill
|
skill_settings (dict): skill
|
||||||
"""
|
"""
|
||||||
if self._is_new_hash(skill_settings['identifier']):
|
if 'skillMetadata' in skill_settings:
|
||||||
self._save_uuid(skill_settings['uuid'])
|
sections = skill_settings['skillMetadata']['sections']
|
||||||
self._save_hash(skill_settings['identifier'])
|
for section in sections:
|
||||||
sections = skill_settings['skillMetadata']['sections']
|
for field in section["fields"]:
|
||||||
for section in sections:
|
if "name" in field and "value" in field:
|
||||||
for field in section["fields"]:
|
# Bypass the change lock to allow server to update
|
||||||
if "name" in field and "value" in field:
|
# during skill init
|
||||||
self[field['name']] = field['value']
|
super(SkillSettings, self).__setitem__(field['name'],
|
||||||
self.store()
|
field['value'])
|
||||||
|
self.store()
|
||||||
def _load_uuid(self):
|
|
||||||
""" Loads uuid
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: uuid of the previous settingsmeta
|
|
||||||
"""
|
|
||||||
directory = self.config.get("skills")["directory"]
|
|
||||||
directory = join(directory, self.name)
|
|
||||||
directory = expanduser(directory)
|
|
||||||
uuid_file = join(directory, 'uuid')
|
|
||||||
uuid = None
|
|
||||||
if isfile(uuid_file):
|
|
||||||
with open(uuid_file, 'r') as f:
|
|
||||||
uuid = f.read()
|
|
||||||
return uuid
|
|
||||||
|
|
||||||
def _save_uuid(self, uuid):
|
|
||||||
""" Saves uuid.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
uuid (str): uuid, unique id of new settingsmeta
|
|
||||||
"""
|
|
||||||
directory = self.config.get("skills")["directory"]
|
|
||||||
directory = join(directory, self.name)
|
|
||||||
directory = expanduser(directory)
|
|
||||||
uuid_file = join(directory, 'uuid')
|
|
||||||
os.makedirs(directory, exist_ok=True)
|
|
||||||
with open(uuid_file, 'w') as f:
|
|
||||||
f.write(str(uuid))
|
|
||||||
|
|
||||||
def _uuid_exist(self):
|
|
||||||
""" Checks if there is an uuid file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if uuid file exist False otherwise
|
|
||||||
"""
|
|
||||||
directory = self.config.get("skills")["directory"]
|
|
||||||
directory = join(directory, self.name)
|
|
||||||
directory = expanduser(directory)
|
|
||||||
uuid_file = join(directory, 'uuid')
|
|
||||||
return isfile(uuid_file)
|
|
||||||
|
|
||||||
def _migrate_settings(self, settings_meta):
|
def _migrate_settings(self, settings_meta):
|
||||||
""" sync settings.json and settingsmeta in memory """
|
""" sync settings.json and settingsmeta in memory """
|
||||||
meta = settings_meta.copy()
|
meta = settings_meta.copy()
|
||||||
|
if 'skillMetadata' not in meta:
|
||||||
|
return meta
|
||||||
self.load_skill_settings_from_file()
|
self.load_skill_settings_from_file()
|
||||||
sections = meta['skillMetadata']['sections']
|
sections = meta['skillMetadata']['sections']
|
||||||
for i, section in enumerate(sections):
|
for i, section in enumerate(sections):
|
||||||
|
@ -314,90 +344,50 @@ class SkillSettings(dict):
|
||||||
meta['skillMetadata']['sections'] = sections
|
meta['skillMetadata']['sections'] = sections
|
||||||
return meta
|
return meta
|
||||||
|
|
||||||
def _upload_meta(self, settings_meta, hashed_meta):
|
def _upload_meta(self, settings_meta, identifier):
|
||||||
""" uploads the new meta data to settings with settings migration
|
""" uploads the new meta data to settings with settings migration
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings_meta (dict): from settingsmeta.json or settingsmeta.yaml
|
settings_meta (dict): settingsmeta.json or settingsmeta.yaml
|
||||||
hashed_meta (str): {skill-folder}-settinsmeta.json
|
identifier (str): identifier for skills meta data
|
||||||
"""
|
"""
|
||||||
|
LOG.debug('Uploading settings meta for {}'.format(identifier))
|
||||||
meta = self._migrate_settings(settings_meta)
|
meta = self._migrate_settings(settings_meta)
|
||||||
meta['identifier'] = str(hashed_meta)
|
meta['identifier'] = identifier
|
||||||
response = self._send_settings_meta(meta)
|
response = self._send_settings_meta(meta)
|
||||||
if response and 'uuid' in response:
|
|
||||||
self._save_uuid(response['uuid'])
|
|
||||||
if 'not_owner' in self:
|
|
||||||
del self['not_owner']
|
|
||||||
self._save_hash(hashed_meta)
|
|
||||||
|
|
||||||
def hash(self, string):
|
def hash(self, string):
|
||||||
""" md5 hasher for consistency across cpu architectures """
|
""" md5 hasher for consistency across cpu architectures """
|
||||||
return hashlib.md5(bytes(string, 'utf-8')).hexdigest()
|
return hashlib.md5(bytes(string, 'utf-8')).hexdigest()
|
||||||
|
|
||||||
def _get_meta_hash(self, settings_meta):
|
|
||||||
""" Gets the hash of skill
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings_meta (dict): settingsmeta object
|
|
||||||
Returns:
|
|
||||||
_hash (str): hashed to identify skills
|
|
||||||
"""
|
|
||||||
_hash = self.hash(json.dumps(settings_meta, sort_keys=True) +
|
|
||||||
self._user_identity)
|
|
||||||
return "{}--{}".format(self.name, _hash)
|
|
||||||
|
|
||||||
def _save_hash(self, hashed_meta):
|
|
||||||
""" Saves hashed_meta to settings directory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
hashed_meta (str): hash of new settingsmeta
|
|
||||||
"""
|
|
||||||
directory = self.config.get("skills")["directory"]
|
|
||||||
directory = join(directory, self.name)
|
|
||||||
directory = expanduser(directory)
|
|
||||||
hash_file = join(directory, 'hash')
|
|
||||||
os.makedirs(directory, exist_ok=True)
|
|
||||||
with open(hash_file, 'w') as f:
|
|
||||||
f.write(hashed_meta)
|
|
||||||
|
|
||||||
def _is_new_hash(self, hashed_meta):
|
|
||||||
""" Check if stored hash is the same as current.
|
|
||||||
|
|
||||||
If the hashed file does not exist, usually in the
|
|
||||||
case of first load, then the create it and return True
|
|
||||||
|
|
||||||
Args:
|
|
||||||
hashed_meta (str): hash of metadata and uuid of device
|
|
||||||
Returns:
|
|
||||||
bool: True if hash is new, otherwise False
|
|
||||||
"""
|
|
||||||
directory = self.config.get("skills")["directory"]
|
|
||||||
directory = join(directory, self.name)
|
|
||||||
directory = expanduser(directory)
|
|
||||||
hash_file = join(directory, 'hash')
|
|
||||||
if isfile(hash_file):
|
|
||||||
with open(hash_file, 'r') as f:
|
|
||||||
current_hash = f.read()
|
|
||||||
return False if current_hash == str(hashed_meta) else True
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update_remote(self):
|
def update_remote(self):
|
||||||
""" update settings state from server """
|
""" update settings state from server """
|
||||||
skills_settings = None
|
|
||||||
settings_meta = self._load_settings_meta()
|
settings_meta = self._load_settings_meta()
|
||||||
if settings_meta is None:
|
if settings_meta is None:
|
||||||
return
|
return
|
||||||
hashed_meta = self._get_meta_hash(settings_meta)
|
# Get settings
|
||||||
if self.get('not_owner'):
|
skills_settings = (self._request_my_settings(self.skill_gid) or
|
||||||
skills_settings = self._request_other_settings(hashed_meta)
|
self._request_other_settings(self.skill_gid))
|
||||||
if not skills_settings:
|
|
||||||
skills_settings = self._request_my_settings(hashed_meta)
|
|
||||||
if skills_settings is not None:
|
if skills_settings is not None:
|
||||||
self.save_skill_settings(skills_settings)
|
self.save_skill_settings(skills_settings)
|
||||||
self.store()
|
|
||||||
else:
|
else:
|
||||||
|
LOG.debug("No Settings on server for {}".format(self.skill_gid))
|
||||||
|
# Settings meta doesn't exist on server push them
|
||||||
settings_meta = self._load_settings_meta()
|
settings_meta = self._load_settings_meta()
|
||||||
self._upload_meta(settings_meta, hashed_meta)
|
self._upload_meta(settings_meta, self.skill_gid)
|
||||||
|
|
||||||
|
def _init_blank_meta(self):
|
||||||
|
""" Send blank settingsmeta to remote. """
|
||||||
|
try:
|
||||||
|
if not is_paired() and self.is_alive:
|
||||||
|
self._blank_poll_timer = Timer(60, self._init_blank_meta)
|
||||||
|
self._blank_poll_timer.daemon = True
|
||||||
|
self._blank_poll_timer.start()
|
||||||
|
else:
|
||||||
|
self.initialize_remote_settings()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Failed to send blank meta: {}'.format(repr(e)))
|
||||||
|
|
||||||
def _poll_skill_settings(self):
|
def _poll_skill_settings(self):
|
||||||
""" If identifier exists for this skill poll to backend to
|
""" If identifier exists for this skill poll to backend to
|
||||||
|
@ -466,6 +456,9 @@ class SkillSettings(dict):
|
||||||
dict: skills object
|
dict: skills object
|
||||||
"""
|
"""
|
||||||
meta = settings_meta.copy()
|
meta = settings_meta.copy()
|
||||||
|
if 'skillMetadata' not in settings_meta:
|
||||||
|
return meta
|
||||||
|
|
||||||
sections = meta['skillMetadata']['sections']
|
sections = meta['skillMetadata']['sections']
|
||||||
|
|
||||||
for i, section in enumerate(sections):
|
for i, section in enumerate(sections):
|
||||||
|
@ -504,7 +497,6 @@ class SkillSettings(dict):
|
||||||
def _request_my_settings(self, identifier):
|
def _request_my_settings(self, identifier):
|
||||||
""" Get skill settings for this device associated
|
""" Get skill settings for this device associated
|
||||||
with the identifier
|
with the identifier
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
identifier (str): a hashed_meta
|
identifier (str): a hashed_meta
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -515,12 +507,33 @@ class SkillSettings(dict):
|
||||||
# this loads the settings into memory for use in self.store
|
# this loads the settings into memory for use in self.store
|
||||||
for skill_settings in settings:
|
for skill_settings in settings:
|
||||||
if skill_settings['identifier'] == identifier:
|
if skill_settings['identifier'] == identifier:
|
||||||
|
LOG.debug("Fetched settings for {}".format(identifier))
|
||||||
skill_settings = \
|
skill_settings = \
|
||||||
self._type_cast(skill_settings, to_platform='core')
|
self._type_cast(skill_settings, to_platform='core')
|
||||||
self._remote_settings = skill_settings
|
self._remote_settings = skill_settings
|
||||||
return skill_settings
|
return skill_settings
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _request_other_settings(self, identifier):
|
||||||
|
""" Retrieve skill settings from other devices by identifier
|
||||||
|
Args:
|
||||||
|
identifier (str): identifier for this skill
|
||||||
|
Returns:
|
||||||
|
settings (dict or None): the retrieved settings or None
|
||||||
|
"""
|
||||||
|
path = \
|
||||||
|
"/" + self._device_identity + "/userSkill?identifier=" + identifier
|
||||||
|
try:
|
||||||
|
user_skill = self.api.request({"method": "GET", "path": path})
|
||||||
|
except RequestException:
|
||||||
|
# Some kind of Timeout, connection HTTPError, etc.
|
||||||
|
user_skill = None
|
||||||
|
if not user_skill or not user_skill[0]:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
settings = self._type_cast(user_skill[0], to_platform='core')
|
||||||
|
return settings
|
||||||
|
|
||||||
def _request_settings(self):
|
def _request_settings(self):
|
||||||
""" Get all skill settings for this device from server.
|
""" Get all skill settings for this device from server.
|
||||||
|
|
||||||
|
@ -538,27 +551,6 @@ class SkillSettings(dict):
|
||||||
settings = [skills for skills in settings if skills is not None]
|
settings = [skills for skills in settings if skills is not None]
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
def _request_other_settings(self, identifier):
|
|
||||||
""" Retrieve skill settings from other devices by identifier
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identifier (str): identifier for this skill
|
|
||||||
Returns:
|
|
||||||
settings (dict or None): the retrieved settings or None
|
|
||||||
"""
|
|
||||||
path = \
|
|
||||||
"/" + self._device_identity + "/userSkill?identifier=" + identifier
|
|
||||||
try:
|
|
||||||
user_skill = self.api.request({"method": "GET", "path": path})
|
|
||||||
except RequestException:
|
|
||||||
# Some kind of Timeout, connection HTTPError, etc.
|
|
||||||
user_skill = None
|
|
||||||
if not user_skill:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
settings = self._type_cast(user_skill[0], to_platform='core')
|
|
||||||
return settings
|
|
||||||
|
|
||||||
def _put_metadata(self, settings_meta):
|
def _put_metadata(self, settings_meta):
|
||||||
""" PUT settingsmeta to backend to be configured in server.
|
""" PUT settingsmeta to backend to be configured in server.
|
||||||
used in place of POST and PATCH.
|
used in place of POST and PATCH.
|
||||||
|
@ -576,6 +568,7 @@ class SkillSettings(dict):
|
||||||
def _delete_metadata(self, uuid):
|
def _delete_metadata(self, uuid):
|
||||||
""" Delete the current skill metadata
|
""" Delete the current skill metadata
|
||||||
|
|
||||||
|
TODO: UPDATE FOR NEW BACKEND
|
||||||
Args:
|
Args:
|
||||||
uuid (str): unique id of the skill
|
uuid (str): unique id of the skill
|
||||||
"""
|
"""
|
||||||
|
@ -594,7 +587,8 @@ class SkillSettings(dict):
|
||||||
@property
|
@property
|
||||||
def _should_upload_from_change(self):
|
def _should_upload_from_change(self):
|
||||||
changed = False
|
changed = False
|
||||||
if hasattr(self, '_remote_settings'):
|
if (hasattr(self, '_remote_settings') and
|
||||||
|
'skillMetadata' in self._remote_settings):
|
||||||
sections = self._remote_settings['skillMetadata']['sections']
|
sections = self._remote_settings['skillMetadata']['sections']
|
||||||
for i, section in enumerate(sections):
|
for i, section in enumerate(sections):
|
||||||
for j, field in enumerate(section['fields']):
|
for j, field in enumerate(section['fields']):
|
||||||
|
@ -624,11 +618,7 @@ class SkillSettings(dict):
|
||||||
|
|
||||||
if self._should_upload_from_change:
|
if self._should_upload_from_change:
|
||||||
settings_meta = self._load_settings_meta()
|
settings_meta = self._load_settings_meta()
|
||||||
hashed_meta = self._get_meta_hash(settings_meta)
|
self._upload_meta(settings_meta, self.skill_gid)
|
||||||
uuid = self._load_uuid()
|
|
||||||
if uuid is not None:
|
|
||||||
self._delete_metadata(uuid)
|
|
||||||
self._upload_meta(settings_meta, hashed_meta)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_meta_path(base_directory):
|
def _get_meta_path(base_directory):
|
||||||
|
|
|
@ -33,7 +33,7 @@ from mycroft.util.log import LOG
|
||||||
from mycroft.api import DeviceApi, is_paired
|
from mycroft.api import DeviceApi, is_paired
|
||||||
|
|
||||||
from .core import load_skill, create_skill_descriptor, MainModule
|
from .core import load_skill, create_skill_descriptor, MainModule
|
||||||
|
from .msm_wrapper import create_msm as msm_creator
|
||||||
|
|
||||||
DEBUG = Configuration.get().get("debug", False)
|
DEBUG = Configuration.get().get("debug", False)
|
||||||
skills_config = Configuration.get().get("skills")
|
skills_config = Configuration.get().get("skills")
|
||||||
|
@ -132,23 +132,7 @@ class SkillManager(Thread):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_msm():
|
def create_msm():
|
||||||
config = Configuration.get()
|
return msm_creator(Configuration.get())
|
||||||
msm_config = config['skills']['msm']
|
|
||||||
repo_config = msm_config['repo']
|
|
||||||
data_dir = expanduser(config['data_dir'])
|
|
||||||
skills_dir = join(data_dir, msm_config['directory'])
|
|
||||||
# Try to create the skills directory if it doesn't exist
|
|
||||||
if not exists(skills_dir):
|
|
||||||
os.makedirs(skills_dir)
|
|
||||||
|
|
||||||
repo_cache = join(data_dir, repo_config['cache'])
|
|
||||||
platform = config['enclosure'].get('platform', 'default')
|
|
||||||
return MycroftSkillsManager(
|
|
||||||
platform=platform, skills_dir=skills_dir,
|
|
||||||
repo=SkillRepo(
|
|
||||||
repo_cache, repo_config['url'], repo_config['branch']
|
|
||||||
), versioned=msm_config['versioned']
|
|
||||||
)
|
|
||||||
|
|
||||||
def schedule_now(self, message=None):
|
def schedule_now(self, message=None):
|
||||||
self.next_download = time.time() - 1
|
self.next_download = time.time() - 1
|
||||||
|
|
|
@ -23,7 +23,7 @@ google-api-python-client==1.6.4
|
||||||
fasteners==0.14.1
|
fasteners==0.14.1
|
||||||
PyYAML==3.13
|
PyYAML==3.13
|
||||||
|
|
||||||
msm==0.7.3
|
msm==0.7.5
|
||||||
msk==0.3.12
|
msk==0.3.12
|
||||||
adapt-parser==0.3.2
|
adapt-parser==0.3.2
|
||||||
padatious==0.4.6
|
padatious==0.4.6
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
|
from mock import MagicMock
|
||||||
|
|
||||||
from os import remove
|
from os import remove
|
||||||
from os.path import join, dirname
|
from os.path import join, dirname
|
||||||
|
@ -21,6 +22,10 @@ from os.path import join, dirname
|
||||||
from mycroft.skills.settings import SkillSettings
|
from mycroft.skills.settings import SkillSettings
|
||||||
|
|
||||||
|
|
||||||
|
SkillSettings._poll_skill_settings = MagicMock()
|
||||||
|
SkillSettings._init_blank_meta = MagicMock()
|
||||||
|
|
||||||
|
|
||||||
class SkillSettingsTest(unittest.TestCase):
|
class SkillSettingsTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import unittest
|
import unittest
|
||||||
import mock
|
import mock
|
||||||
|
import copy
|
||||||
import tempfile
|
import tempfile
|
||||||
from os.path import exists, join
|
from os.path import exists, join
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
|
@ -9,7 +10,6 @@ from mycroft.configuration import Configuration
|
||||||
from mycroft.skills.skill_manager import SkillManager
|
from mycroft.skills.skill_manager import SkillManager
|
||||||
|
|
||||||
BASE_CONF = base_config()
|
BASE_CONF = base_config()
|
||||||
BASE_CONF['data_dir'] = tempfile.mkdtemp()
|
|
||||||
BASE_CONF['skills'] = {
|
BASE_CONF['skills'] = {
|
||||||
'msm': {
|
'msm': {
|
||||||
'directory': 'skills',
|
'directory': 'skills',
|
||||||
|
@ -17,7 +17,7 @@ BASE_CONF['skills'] = {
|
||||||
'repo': {
|
'repo': {
|
||||||
'cache': '.skills-repo',
|
'cache': '.skills-repo',
|
||||||
'url': 'https://github.com/MycroftAI/mycroft-skills',
|
'url': 'https://github.com/MycroftAI/mycroft-skills',
|
||||||
'branch': '18.08'
|
'branch': '19.02'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'update_interval': 3600,
|
'update_interval': 3600,
|
||||||
|
@ -56,14 +56,16 @@ class MycroftSkillTest(unittest.TestCase):
|
||||||
self.emitter.reset()
|
self.emitter.reset()
|
||||||
self.temp_dir = tempfile.mkdtemp()
|
self.temp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
@mock.patch.dict(Configuration._Configuration__config, BASE_CONF)
|
|
||||||
def test_create_manager(self):
|
def test_create_manager(self):
|
||||||
""" Verify that the skill manager and msm loads as expected and
|
""" Verify that the skill manager and msm loads as expected and
|
||||||
that the skills dir is created as needed.
|
that the skills dir is created as needed.
|
||||||
"""
|
"""
|
||||||
SkillManager(self.emitter)
|
conf = copy.deepcopy(BASE_CONF)
|
||||||
self.assertTrue(exists(join(BASE_CONF['data_dir'], 'skills')))
|
conf['data_dir'] = self.temp_dir
|
||||||
|
with mock.patch.dict(Configuration._Configuration__config,
|
||||||
|
BASE_CONF):
|
||||||
|
SkillManager(self.emitter)
|
||||||
|
self.assertTrue(exists(join(BASE_CONF['data_dir'], 'skills')))
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self):
|
||||||
def tearDownClass(cls):
|
rmtree(self.temp_dir)
|
||||||
rmtree(BASE_CONF['data_dir'])
|
|
||||||
|
|
|
@ -15,23 +15,25 @@
|
||||||
import unittest
|
import unittest
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
|
|
||||||
from os.path import exists, isfile
|
from os.path import exists, isfile, join
|
||||||
|
from tempfile import gettempdir
|
||||||
|
|
||||||
from mycroft.util import create_signal, check_for_signal
|
from mycroft.util import create_signal, check_for_signal
|
||||||
|
|
||||||
|
|
||||||
class TestSignals(unittest.TestCase):
|
class TestSignals(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
if exists('/tmp/mycroft'):
|
if exists(join(gettempdir(), 'mycroft')):
|
||||||
rmtree('/tmp/mycroft')
|
rmtree(join(gettempdir(), 'mycroft'))
|
||||||
|
|
||||||
def test_create_signal(self):
|
def test_create_signal(self):
|
||||||
create_signal('test_signal')
|
create_signal('test_signal')
|
||||||
self.assertTrue(isfile('/tmp/mycroft/ipc/signal/test_signal'))
|
self.assertTrue(isfile(join(gettempdir(),
|
||||||
|
'mycroft/ipc/signal/test_signal')))
|
||||||
|
|
||||||
def test_check_signal(self):
|
def test_check_signal(self):
|
||||||
if exists('/tmp/mycroft'):
|
if exists(join(gettempdir(), 'mycroft')):
|
||||||
rmtree('/tmp/mycroft')
|
rmtree(join(gettempdir(), 'mycroft'))
|
||||||
# check that signal is not found if file does not exist
|
# check that signal is not found if file does not exist
|
||||||
self.assertFalse(check_for_signal('test_signal'))
|
self.assertFalse(check_for_signal('test_signal'))
|
||||||
|
|
||||||
|
@ -39,7 +41,8 @@ class TestSignals(unittest.TestCase):
|
||||||
create_signal('test_signal')
|
create_signal('test_signal')
|
||||||
self.assertTrue(check_for_signal('test_signal'))
|
self.assertTrue(check_for_signal('test_signal'))
|
||||||
# Check that the signal is removed after use
|
# Check that the signal is removed after use
|
||||||
self.assertFalse(isfile('/tmp/mycroft/ipc/signal/test_signal'))
|
self.assertFalse(isfile(join(gettempdir(),
|
||||||
|
'mycroft/ipc/signal/test_signal')))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
Loading…
Reference in New Issue