Upstream pull-request: https://github.com/MycroftAI/mycroft-core/pull/2559 diff --git a/mycroft/skills/mycroft_skill/mycroft_skill.py b/mycroft/skills/mycroft_skill/mycroft_skill.py index 4f3b6d9da6..d60a3279d0 100644 --- a/mycroft/skills/mycroft_skill/mycroft_skill.py +++ b/mycroft/skills/mycroft_skill/mycroft_skill.py @@ -21,8 +21,11 @@ import traceback from itertools import chain from os import walk from os.path import join, abspath, dirname, basename, exists +from pathlib import Path from threading import Event, Timer +from xdg import BaseDirectory + from adapt.intent import Intent, IntentBuilder from mycroft import dialog @@ -113,6 +116,7 @@ class MycroftSkill: bus (MycroftWebsocketClient): Optional bus connection use_settings (bool): Set to false to not use skill settings at all """ + def __init__(self, name=None, bus=None, use_settings=True): self.name = name or self.__class__.__name__ self.resting_name = None @@ -123,16 +127,6 @@ class MycroftSkill: #: Member variable containing the absolute path of the skill's root #: directory. E.g. /opt/mycroft/skills/my-skill.me/ self.root_dir = dirname(abspath(sys.modules[self.__module__].__file__)) - if use_settings: - self.settings = get_local_settings(self.root_dir, self.name) - self._initial_settings = deepcopy(self.settings) - else: - self.settings = None - - #: Set to register a callback method that will be called every time - #: the skills settings are updated. The referenced method should - #: include any logic needed to handle the updated settings. - self.settings_change_callback = None self.gui = SkillGUI(self) @@ -141,6 +135,19 @@ class MycroftSkill: self.bind(bus) #: Mycroft global configuration. (dict) self.config_core = Configuration.get() + + self.settings = None + self.settings_write_path = None + self.settings_read_path = None + + if use_settings: + self._init_settings() + + #: Set to register a callback method that will be called every time + #: the skills settings are updated. The referenced method should + #: include any logic needed to handle the updated settings. + self.settings_change_callback = None + self.dialog_renderer = None #: Filesystem access to skill specific folder. @@ -157,6 +164,42 @@ class MycroftSkill: self.event_scheduler = EventSchedulerInterface(self.name) self.intent_service = IntentServiceInterface() + def _init_settings(self): + """Setup skill settings.""" + + # To not break existing setups, + # save to skill directory if the file exists already + self.settings_write_path = Path(self.root_dir) + + # Otherwise save to XDG_CONFIG_DIR + if not self.settings_write_path.joinpath('settings.json').exists(): + self.settings_write_path = Path(BaseDirectory.save_config_path( + 'mycroft', 'skills', basename(self.root_dir))) + + # To not break existing setups, + # read from skill directory if the settings file exists there + self.settings_read_path = Path(self.root_dir) + + # Then, check XDG_CONFIG_DIR + if not self.settings_read_path.joinpath('settings.json').exists(): + for dir in BaseDirectory.load_config_paths('mycroft', + 'skills', + basename( + self.root_dir)): + path = Path(dir) + #: If there is a settings file here, use it + if path.joinpath('settings.json').exists(): + self.settings_read_path = path + break + + # Lastly, check /etc/mycroft + if not self.settings_read_path.joinpath('settings.json').exists(): + self.settings_read_path = Path( + '/etc/mycroft/skills/').joinpath(basename(self.root_dir)) + + self.settings = get_local_settings(self.settings_read_path, self.name) + self._initial_settings = deepcopy(self.settings) + @property def enclosure(self): if self._enclosure: @@ -271,7 +314,7 @@ class MycroftSkill: if remote_settings is not None: LOG.info('Updating settings for skill ' + self.name) self.settings.update(**remote_settings) - save_settings(self.root_dir, self.settings) + save_settings(self.settings_write_path, self.settings) if self.settings_change_callback is not None: self.settings_change_callback() @@ -544,7 +587,7 @@ class MycroftSkill: if not voc or not exists(voc): raise FileNotFoundError( - 'Could not find {}.voc file'.format(voc_filename)) + 'Could not find {}.voc file'.format(voc_filename)) # load vocab and flatten into a simple list vocab = read_vocab_file(voc) self.voc_match_cache[cache_key] = list(chain(*vocab)) @@ -811,7 +854,7 @@ class MycroftSkill: """Store settings and indicate that the skill handler has completed """ if self.settings != self._initial_settings: - save_settings(self.root_dir, self.settings) + save_settings(self.settings_write_path, self.settings) self._initial_settings = self.settings if handler_info: msg_type = handler_info + '.complete' @@ -1233,7 +1276,7 @@ class MycroftSkill: # Store settings if self.settings != self._initial_settings: - save_settings(self.root_dir, self.settings) + save_settings(self.settings_write_path, self.settings) if self.settings_meta: self.settings_meta.stop() diff --git a/mycroft/skills/settings.py b/mycroft/skills/settings.py index c210625f5b..f576b34c9e 100644 --- a/mycroft/skills/settings.py +++ b/mycroft/skills/settings.py @@ -93,18 +93,22 @@ def get_local_settings(skill_dir, skill_name) -> dict: def save_settings(skill_dir, skill_settings): """Save skill settings to file.""" settings_path = Path(skill_dir).joinpath('settings.json') - if Path(skill_dir).exists(): - with open(str(settings_path), 'w') as settings_file: - try: - json.dump(skill_settings, settings_file) - except Exception: - LOG.exception('error saving skill settings to ' - '{}'.format(settings_path)) - else: - LOG.info('Skill settings successfully saved to ' - '{}' .format(settings_path)) - else: - LOG.info('Skill folder no longer exists, can\'t save settings.') + + # Either the file already exists in /opt, or we are writing + # to XDG_CONFIG_DIR and always have the permission to make + # sure the file always exists + if not Path(settings_path).exists(): + settings_path.touch(mode=0o644) + + with open(str(settings_path), 'w') as settings_file: + try: + json.dump(skill_settings, settings_file) + except Exception: + LOG.exception('error saving skill settings to ' + '{}'.format(settings_path)) + else: + LOG.info('Skill settings successfully saved to ' + '{}' .format(settings_path)) def get_display_name(skill_name: str): diff --git a/mycroft/skills/skill_loader.py b/mycroft/skills/skill_loader.py index 2402740188..b7e8186967 100644 --- a/mycroft/skills/skill_loader.py +++ b/mycroft/skills/skill_loader.py @@ -259,7 +259,8 @@ class SkillLoader: if first_run: LOG.info("First run of " + self.skill_id) self.instance.settings["__mycroft_skill_firstrun"] = False - save_settings(self.skill_directory, self.instance.settings) + save_settings(self.instance.settings_write_path, + self.instance.settings) intro = self.instance.get_intro_message() if intro: self.instance.speak(intro) diff --git a/requirements.txt b/requirements.txt index 509436fb89..c9358db687 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,4 @@ fann2==1.0.7 padaos==0.1.9 precise-runner==0.2.1 petact==0.1.2 +pyxdg==0.26