mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-04-02 00:07:16 +01:00
Guard shutdown() for STOPPING/CLOSE and unload host early in stop()
Move host.unload() before Stage 1 in stop() so the shutdown monitor task is cancelled before infrastructure teardown begins. This prevents a race where the monitor could react to PrepareForShutdown after stop() has already started tearing down. For the remaining edge case where shutdown() is called while stop() is running, log a warning and return immediately instead of awaiting the shutdown event (which would deadlock since stop() never sets it). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -315,6 +315,9 @@ class Core(CoreSysAttributes):
|
||||
# don't process scheduler anymore
|
||||
await self.set_state(CoreState.STOPPING)
|
||||
|
||||
# Cancel shutdown monitor task before tearing down infrastructure
|
||||
await self.sys_host.unload()
|
||||
|
||||
# Stage 1
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
@@ -341,7 +344,6 @@ class Core(CoreSysAttributes):
|
||||
self.sys_websession.close(),
|
||||
self.sys_ingress.unload(),
|
||||
self.sys_hardware.unload(),
|
||||
self.sys_host.unload(),
|
||||
self.sys_dbus.unload(),
|
||||
)
|
||||
]
|
||||
@@ -359,7 +361,13 @@ class Core(CoreSysAttributes):
|
||||
Reentrant: if a shutdown is already in progress, subsequent calls
|
||||
await completion of the existing shutdown rather than starting a second one.
|
||||
"""
|
||||
if self.state in (CoreState.SHUTDOWN, CoreState.STOPPING, CoreState.CLOSE):
|
||||
# Supervisor is already tearing down, no point running shutdown
|
||||
if self.state in (CoreState.STOPPING, CoreState.CLOSE):
|
||||
_LOGGER.warning("Ignoring shutdown request, Supervisor is already stopping")
|
||||
return
|
||||
|
||||
# Another shutdown is in progress, wait for it to complete
|
||||
if self.state == CoreState.SHUTDOWN:
|
||||
await self._shutdown_event.wait()
|
||||
return
|
||||
|
||||
|
||||
@@ -211,3 +211,19 @@ async def test_shutdown_event_reset_between_cycles(coresys: CoreSys):
|
||||
|
||||
assert second_entered
|
||||
assert coresys.core._shutdown_event.is_set()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"state", [CoreState.STOPPING, CoreState.CLOSE], ids=["stopping", "close"]
|
||||
)
|
||||
async def test_shutdown_ignored_during_stop(
|
||||
coresys: CoreSys, caplog: pytest.LogCaptureFixture, state: CoreState
|
||||
):
|
||||
"""Test that shutdown is ignored when Supervisor is already stopping."""
|
||||
await coresys.core.set_state(state)
|
||||
|
||||
with patch.object(coresys.addons, "shutdown") as mock_addon_shutdown:
|
||||
await coresys.core.shutdown()
|
||||
|
||||
mock_addon_shutdown.assert_not_called()
|
||||
assert "Ignoring shutdown request, Supervisor is already stopping" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user