diff --git a/neon_core/__init__.py b/neon_core/__init__.py index dc45a4b5b..837699d77 100644 --- a/neon_core/__init__.py +++ b/neon_core/__init__.py @@ -31,6 +31,8 @@ environ["OVOS_DEFAULT_CONFIG"] = join(dirname(__file__), "configuration", "neon.yaml") +environ.setdefault('OVOS_CONFIG_BASE_FOLDER', "neon") +environ.setdefault('OVOS_CONFIG_FILENAME', "neon.yaml") # Patching deprecation warnings # TODO: Deprecate after migration to ovos-workshop 1.0+ and ovos-core 0.3.0 diff --git a/neon_core/skills/skill_manager.py b/neon_core/skills/skill_manager.py index c2d5b86bd..9853cdb86 100644 --- a/neon_core/skills/skill_manager.py +++ b/neon_core/skills/skill_manager.py @@ -28,24 +28,46 @@ from os import makedirs from os.path import isdir, join, expanduser +from threading import Thread from ovos_utils.xdg_utils import xdg_data_home -from ovos_utils.log import LOG - +from ovos_utils.log import LOG, deprecated +from ovos_bus_client.message import Message from ovos_core.skill_manager import SkillManager class NeonSkillManager(SkillManager): + def _sync_skill_loading_state(self): + """ + Override to wait for configured ready settings before announcing the + service is ready + """ + SkillManager._sync_skill_loading_state(self) + LOG.info( + "Waiting for skill ready settings" + ) # TODO Log is only for debugging + self._wait_until_skills_ready() + + # Start a background thread to check for configured ready settings + # while allowing the skills service to continue initialization + ready_event_thread = Thread(target=self._check_device_ready) + ready_event_thread.daemon = True + ready_event_thread.start() + @deprecated("Legacy skills are deprecated and this method should not " + "be used.", "25.10.1") def get_default_skills_dir(self): """ Go through legacy config params to locate the default skill directory """ skill_config = self.config["skills"] - skill_dir = skill_config.get("directory") or \ - skill_config.get("extra_directories") - skill_dir = skill_dir[0] if isinstance(skill_dir, list) and \ - len(skill_dir) > 0 else skill_dir or \ - join(xdg_data_home(), "neon", "skills") + skill_dir = skill_config.get("directory") or skill_config.get( + "extra_directories" + ) + skill_dir = ( + skill_dir[0] + if isinstance(skill_dir, list) and len(skill_dir) > 0 + else skill_dir or join(xdg_data_home(), "neon", "skills") + ) skill_dir = expanduser(skill_dir) if not isdir(skill_dir): @@ -61,20 +83,70 @@ def get_default_skills_dir(self): return skill_dir - def _load_new_skills(self, *args, **kwargs): - # Override load method for config module checks - SkillManager._load_new_skills(self, *args, **kwargs) - def _get_plugin_skill_loader(self, skill_id, init_bus=True): assert self.bus is not None if not init_bus: LOG.debug("Ignoring request not to bind bus") return SkillManager._get_plugin_skill_loader(self, skill_id, True) - def run(self): - """Load skills and update periodically from disk and internet.""" - from os import environ - environ.setdefault('OVOS_CONFIG_BASE_FOLDER', "neon") - environ.setdefault('OVOS_CONFIG_FILENAME', "neon.yaml") - LOG.debug("set default configuration to `neon/neon.yaml`") - SkillManager.run(self) + # Re-implement support for internet and network skill load + def _wait_until_skills_ready(self): + """ + Block until configured network and internet skills are loaded to + delay skills service reporting ready. + """ + ready_settings = self.config.get("ready_settings", ["skills"]) + if "network_skills" in ready_settings: + if not self._network_loaded.wait(self._network_skill_timeout): + LOG.error("Timeout waiting for network skills to load") + return False + if "internet_skills" in ready_settings: + if not self._internet_loaded.wait(self._network_skill_timeout): + LOG.error("Timeout waiting for internet skills to load") + return False + LOG.debug("Configured skill load conditions met") + return True + + def _check_device_ready(self): + while not self._wait_until_skills_ready(): + LOG.warning("Skills not ready, still waiting...") + ready_settings = self.config.get("ready_settings", ["skills"]) + valid_services = ( + "skills", + "voice", + "audio", + "gui_service", + "internet", + ) + ready_services = { + s: False for s in ready_settings if s in valid_services + } + LOG.info(f"Waiting for services: {ready_services}") + while not all(ready_services.values()): + for service in ready_services: + if not ready_services[service]: + resp = self.bus.wait_for_response( + Message( + f"mycroft.{service}.is_ready", + context={ + "source": ["skills"], + "destination": [service], + }, + ) + ) + LOG.debug( + resp.data + if resp + else f"No response for service={service}" + ) + service_ready = resp and resp.data.get("status") + if service_ready: + LOG.info(f"{service} reports ready") + ready_services[service] = service_ready + LOG.info(f"All configured ready settings met: {ready_services}") + self.bus.emit( + Message( + "mycroft.ready", + context={"source": ["skills"], "destination": valid_services}, + ) + ) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 6c0b56062..b6e2910d0 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,4 @@ -# ovos-core version pinned for compat. with patches in NeonCore -ovos-core[lgpl]~=0.1 +ovos-core[lgpl]~=0.2 neon-utils[network]~=1.13,>=1.13.1a4 diff --git a/test/test_configuration.py b/test/test_configuration.py index f0e787df3..f8b6483ef 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -37,8 +37,6 @@ class ConfigurationTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: os.environ["XDG_CONFIG_HOME"] = cls.CONFIG_PATH - os.environ["OVOS_CONFIG_BASE_FOLDER"] = "neon" - os.environ["OVOS_CONFIG_FILENAME"] = "neon.yaml" @classmethod def tearDownClass(cls) -> None: diff --git a/test/test_skills_module.py b/test/test_skills_module.py index 331f27e7d..283002ded 100644 --- a/test/test_skills_module.py +++ b/test/test_skills_module.py @@ -32,12 +32,12 @@ import unittest import wave +from pytest import mark +from mock import Mock, patch from copy import deepcopy from os.path import join, dirname, expanduser, isdir from threading import Event from time import time - -from unittest.mock import Mock, patch from ovos_bus_client import Message from ovos_utils.messagebus import FakeBus from ovos_utils.xdg_utils import xdg_data_home @@ -67,8 +67,6 @@ class TestSkillService(unittest.TestCase): @classmethod def setUpClass(cls) -> None: os.environ["XDG_CONFIG_HOME"] = cls.config_dir - os.environ["OVOS_CONFIG_BASE_FOLDER"] = "neon" - os.environ["OVOS_CONFIG_FILENAME"] = "neon.yaml" @classmethod def tearDownClass(cls) -> None: @@ -184,8 +182,6 @@ def setUpClass(cls) -> None: import neon_core os.environ["XDG_CONFIG_HOME"] = cls.test_config_dir - os.environ["OVOS_CONFIG_BASE_FOLDER"] = "neon" - os.environ["OVOS_CONFIG_FILENAME"] = "neon.yaml" import ovos_config import importlib importlib.reload(ovos_config.meta) @@ -209,8 +205,6 @@ def setUpClass(cls) -> None: def tearDownClass(cls) -> None: cls.intent_service.shutdown() os.environ.pop("XDG_CONFIG_HOME") - os.environ.pop("OVOS_CONFIG_BASE_FOLDER") - os.environ.pop("OVOS_CONFIG_FILENAME") shutil.rmtree(cls.test_config_dir) def test_save_utterance_transcription(self): @@ -357,21 +351,15 @@ class TestSkillManager(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - #from neon_core.util.runtime_utils import use_neon_core - #from neon_utils.configuration_utils import init_config_dir os.environ["XDG_CONFIG_HOME"] = cls.config_dir - os.environ["OVOS_CONFIG_BASE_FOLDER"] = "neon" - os.environ["OVOS_CONFIG_FILENAME"] = "neon.yaml" - #use_neon_core(init_config_dir)() @classmethod def tearDownClass(cls) -> None: os.environ.pop("XDG_CONFIG_HOME") - os.environ.pop("OVOS_CONFIG_BASE_FOLDER") - os.environ.pop("OVOS_CONFIG_FILENAME") if os.path.isdir(cls.config_dir): shutil.rmtree(cls.config_dir) + @mark.skip("Skill directory handling is deprecated") @patch("ovos_core.skill_manager.SkillManager.run") def test_get_default_skills_dir(self, _): from neon_core.skills.skill_manager import NeonSkillManager @@ -410,5 +398,40 @@ def test_get_default_skills_dir(self, _): self.assertEqual(default_dir, expanduser('~/neon-skills')) self.assertTrue(isdir(expanduser("~/neon-skills"))) + def test_wait_until_skills_ready(self): + from neon_core.skills.skill_manager import NeonSkillManager + manager = NeonSkillManager(FakeBus()) + manager._network_skill_timeout = 1 + + # No ready settings is ready + manager.config['ready_settings'] = [] + self.assertTrue(manager._wait_until_skills_ready()) + + # Check network skills not ready + manager.config['ready_settings'] = ['network_skills'] + self.assertFalse(manager._wait_until_skills_ready()) + + # Check internet skills not ready + manager.config['ready_settings'].append('internet_skills') + self.assertFalse(manager._wait_until_skills_ready()) + + # Check skills are loaded + manager._network_loaded.set() + manager._internet_loaded.set() + self.assertTrue(manager._wait_until_skills_ready()) + + def test_check_device_ready(self): + from neon_core.skills.skill_manager import NeonSkillManager + manager = NeonSkillManager(FakeBus()) + + on_ready = Mock() + manager.bus.on("mycroft.ready", on_ready) + + # No services to wait for + manager.config['ready_settings'] = [] + manager._check_device_ready() + on_ready.assert_called_once() + + if __name__ == "__main__": unittest.main()