Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 13 additions & 17 deletions appdaemon/app_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 1 addition & 6 deletions appdaemon/appdaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,20 +367,15 @@ def start(self) -> None:
"""Start AppDaemon, which also starts all the component subsystems like the scheduler, etc.

- :meth:`ThreadAsync <appdaemon.thread_async.ThreadAsync.start>`
- :meth:`Scheduler <appdaemon.scheduler.Scheduler.start>`
- :meth:`Utility <appdaemon.utility_loop.Utility.start>`
- :meth:`AppManagement <appdaemon.app_management.AppManagement.start>`

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.

Expand Down
16 changes: 15 additions & 1 deletion appdaemon/utility_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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:

Expand Down
48 changes: 48 additions & 0 deletions tests/functional/test_production_mode.py
Original file line number Diff line number Diff line change
@@ -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"
Loading