mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-05-08 17:08:36 +01:00
Take shutdown inhibitor lock for graceful service teardown
Handle external shutdown events (ACPI power button, hypervisor shutdown) by listening to logind's PrepareForShutdown signal. A delay inhibitor lock is acquired on startup so Supervisor has time to gracefully stop all managed services before the host proceeds with shutdown. Changes: - Add inhibit() and prepare_for_shutdown() methods to Logind D-Bus interface - Enable Unix FD negotiation on the D-Bus message bus for inhibitor lock FDs - Add background monitor task in HostManager that listens for the signal - Track the monitor task and cancel it cleanly via new unload() method - Wire host unload into Core.stop() stage 2 for clean shutdown - Add PrepareForShutdown signal constant to dbus/const.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,29 @@
|
||||
"""Test host manager."""
|
||||
|
||||
from unittest.mock import patch
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import pytest
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.const import MulticastProtocolEnabled
|
||||
|
||||
from tests.dbus_service_mocks.base import DBusServiceMock
|
||||
from tests.dbus_service_mocks.logind import Logind as LogindService
|
||||
from tests.dbus_service_mocks.rauc import Rauc as RaucService
|
||||
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
|
||||
|
||||
|
||||
@pytest.fixture(name="logind_service")
|
||||
async def fixture_logind_service(
|
||||
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||
) -> LogindService:
|
||||
"""Return logind service mock."""
|
||||
yield all_dbus_services["logind"]
|
||||
|
||||
|
||||
@pytest.fixture(name="systemd_service")
|
||||
async def fixture_systemd_service(
|
||||
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||
@@ -71,3 +82,49 @@ async def test_reload_os(
|
||||
await coresys.host.reload()
|
||||
|
||||
assert rauc_service.GetSlotStatus.calls == [()]
|
||||
|
||||
|
||||
async def test_host_shutdown_on_prepare_for_shutdown_signal(
|
||||
coresys: CoreSys, logind_service: LogindService
|
||||
):
|
||||
"""Test graceful shutdown when PrepareForShutdown signal is received."""
|
||||
shutdown_called = asyncio.Event()
|
||||
|
||||
async def mock_shutdown(**kwargs):
|
||||
shutdown_called.set()
|
||||
|
||||
await coresys.host.load()
|
||||
await coresys.core.set_state(CoreState.RUNNING)
|
||||
|
||||
# Give the monitor task time to start and register the signal listener
|
||||
# (needs multiple yields for inhibit D-Bus call + AddMatch call)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
with patch.object(coresys.core, "shutdown", side_effect=mock_shutdown):
|
||||
# Emit PrepareForShutdown(true) signal as if host is shutting down
|
||||
logind_service.PrepareForShutdown()
|
||||
await logind_service.ping()
|
||||
|
||||
async with asyncio.timeout(2):
|
||||
await shutdown_called.wait()
|
||||
|
||||
|
||||
async def test_host_shutdown_signal_ignored_when_not_running(
|
||||
coresys: CoreSys, logind_service: LogindService
|
||||
):
|
||||
"""Test PrepareForShutdown is ignored if Supervisor already shutting down."""
|
||||
await coresys.host.load()
|
||||
await coresys.core.set_state(CoreState.SHUTDOWN)
|
||||
|
||||
# Give the monitor task time to start and register the signal listener
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
with patch.object(
|
||||
coresys.core, "shutdown", new_callable=AsyncMock
|
||||
) as mock_shutdown:
|
||||
logind_service.PrepareForShutdown()
|
||||
await logind_service.ping()
|
||||
# Give the monitor task time to process the signal
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
mock_shutdown.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user