diff --git a/appdaemon/app_management.py b/appdaemon/app_management.py index 11d7a745f..fb87c5d1f 100644 --- a/appdaemon/app_management.py +++ b/appdaemon/app_management.py @@ -163,28 +163,24 @@ def sequence_config(self) -> SequenceConfig | None: def valid_apps(self) -> set[str]: return self.running_apps | self.loaded_globals - def start(self) -> None: - """Start the app management subsystem, which creates async tasks to - - * Initialize admin entities - * Call :meth:`~.check_app_updates` - * Fire an ``appd_started`` event in the ``global`` namespace. + async def start(self) -> None: + """Start the app management subsystem. + This method: + * Initializes admin entities + * Initializes the dependency manager (INIT mode) + * Loads all apps (normal mode) """ if self.AD.apps_enabled: self.logger.debug("Starting the app management subsystem") - self.AD.loop.create_task(self.init_admin_entities()) + await self.init_admin_entities() - task = self.AD.loop.create_task( - self.check_app_updates(mode=UpdateMode.INIT), - name="check_app_updates", - ) - task.add_done_callback( - lambda _: self.AD.loop.create_task( - self.AD.events.process_event("global", {"event_type": "appd_started", "data": {}}), - name="appd_started_event" - ) - ) + await self.check_app_updates(mode=UpdateMode.INIT) + + self.logger.debug("Loading apps") + await self.check_app_updates() + + self.logger.info("App initialization complete") async def stop(self) -> None: """Stop the app management subsystem and all the running apps. diff --git a/appdaemon/appdaemon.py b/appdaemon/appdaemon.py index 623c23ecd..33e8abf6a 100755 --- a/appdaemon/appdaemon.py +++ b/appdaemon/appdaemon.py @@ -367,20 +367,15 @@ def start(self) -> None: """Start AppDaemon, which also starts all the component subsystems like the scheduler, etc. - :meth:`ThreadAsync ` - - :meth:`Scheduler ` - :meth:`Utility ` - - :meth:`AppManagement ` + Note: The scheduler is started by the utility loop after plugins are ready. """ self.logger.debug("Starting AppDaemon") self.thread_async.start() - self.sched.start() self.utility.start() self.state.start() - if self.apps_enabled: - self.app_management.start() - async def stop(self) -> None: """Stop AppDaemon by calling the stop method of the subsystems. diff --git a/appdaemon/utility_loop.py b/appdaemon/utility_loop.py index 34d492acd..3dacb6de2 100644 --- a/appdaemon/utility_loop.py +++ b/appdaemon/utility_loop.py @@ -143,7 +143,8 @@ async def _init_loop(self): * Starts the web server if configured * Waits for all plugins to initialize * Registers services - * Runs check_app_updates with UpdateMode.INIT if apps are enabled + * Starts the scheduler + * Initializes apps if apps are enabled """ self.logger.debug("Starting utility loop") @@ -158,8 +159,21 @@ async def _init_loop(self): # Wait for all plugins to initialize await self.AD.plugins.wait_for_plugins() + if self.AD.stopping: + self.logger.debug("AppDaemon already stopping before starting utility loop") + return + await self._register_services() + # Start the scheduler + self.AD.sched.start() + + if self.AD.apps_enabled: + await self.AD.app_management.start() + + # Fire APPD Started Event + await self.AD.events.process_event("global", {"event_type": "appd_started", "data": {}}) + async def loop(self): """Run the utility loop, which handles the following: diff --git a/tests/functional/test_production_mode.py b/tests/functional/test_production_mode.py new file mode 100644 index 000000000..f9483b488 --- /dev/null +++ b/tests/functional/test_production_mode.py @@ -0,0 +1,48 @@ +import os +from unittest.mock import AsyncMock + +import pytest +import pytest_asyncio +from appdaemon.appdaemon import AppDaemon + + +@pytest_asyncio.fixture(scope="function") +async def ad_production(ad_obj: AppDaemon): + """AppDaemon fixture with production_mode enabled.""" + ad_obj.config.production_mode = True + ad_obj.app_dir = ad_obj.config_dir / "apps/hello_world" + + ad_obj.start() + yield ad_obj + await ad_obj.stop() + + +@pytest.mark.ci +@pytest.mark.functional +@pytest.mark.asyncio(loop_scope="session") +async def test_production_mode_loads_apps(ad_production: AppDaemon) -> None: + """Test that apps load correctly when production_mode is enabled.""" + # Wait for initialization to complete + await ad_production.utility.app_update_event.wait() + # Check that the app loaded + assert "hello_world" in ad_production.app_management.objects + + +@pytest.mark.ci +@pytest.mark.functional +@pytest.mark.asyncio(loop_scope="session") +async def test_production_mode_no_reloading(ad_production: AppDaemon) -> None: + """Test that production mode doesn't reload apps when files change.""" + # Wait for initialization to complete + await ad_production.utility.app_update_event.wait() + + # Mock check_app_updates to track calls from now on + mock = AsyncMock(wraps=ad_production.app_management.check_app_updates) + ad_production.app_management.check_app_updates = mock + + # Touch file and wait for utility loop + ad_production.utility.app_update_event.clear() + os.utime(ad_production.app_dir / "hello.py", None) + await ad_production.utility.app_update_event.wait() + + assert not mock.called, "Should not reload in production mode"