mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-04-17 23:33:35 +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
|
# don't process scheduler anymore
|
||||||
await self.set_state(CoreState.STOPPING)
|
await self.set_state(CoreState.STOPPING)
|
||||||
|
|
||||||
|
# Cancel shutdown monitor task before tearing down infrastructure
|
||||||
|
await self.sys_host.unload()
|
||||||
|
|
||||||
# Stage 1
|
# Stage 1
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(10):
|
async with asyncio.timeout(10):
|
||||||
@@ -341,7 +344,6 @@ class Core(CoreSysAttributes):
|
|||||||
self.sys_websession.close(),
|
self.sys_websession.close(),
|
||||||
self.sys_ingress.unload(),
|
self.sys_ingress.unload(),
|
||||||
self.sys_hardware.unload(),
|
self.sys_hardware.unload(),
|
||||||
self.sys_host.unload(),
|
|
||||||
self.sys_dbus.unload(),
|
self.sys_dbus.unload(),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -359,7 +361,13 @@ class Core(CoreSysAttributes):
|
|||||||
Reentrant: if a shutdown is already in progress, subsequent calls
|
Reentrant: if a shutdown is already in progress, subsequent calls
|
||||||
await completion of the existing shutdown rather than starting a second one.
|
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()
|
await self._shutdown_event.wait()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -211,3 +211,19 @@ async def test_shutdown_event_reset_between_cycles(coresys: CoreSys):
|
|||||||
|
|
||||||
assert second_entered
|
assert second_entered
|
||||||
assert coresys.core._shutdown_event.is_set()
|
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