1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-04-02 00:07:16 +01:00

Make core.shutdown() reentrant and remove monitor state guard

Make Core.shutdown() reentrant using an asyncio.Event so that
concurrent callers await the in-progress shutdown instead of
starting a second one. This ensures the inhibitor lock is held
until shutdown truly completes, even if PrepareForShutdown fires
while an API-initiated shutdown is already in progress.

The state guard in the monitor task is no longer needed since
core.shutdown() now handles reentrance itself.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Stefan Agner
2026-03-13 17:25:34 +01:00
parent d8fe08b70a
commit 2edd7425b3
3 changed files with 40 additions and 33 deletions

View File

@@ -42,6 +42,7 @@ class Core(CoreSysAttributes):
self.coresys: CoreSys = coresys
self._state: CoreState = CoreState.INITIALIZE
self.exit_code: int = 0
self._shutdown_event: asyncio.Event = asyncio.Event()
@property
def state(self) -> CoreState:
@@ -353,28 +354,39 @@ class Core(CoreSysAttributes):
self.sys_loop.stop()
async def shutdown(self, *, remove_homeassistant_container: bool = False) -> None:
"""Shutdown all running containers in correct order."""
"""Shutdown all running containers in correct order.
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):
await self._shutdown_event.wait()
return
# don't process scheduler anymore
if self.state == CoreState.RUNNING:
await self.set_state(CoreState.SHUTDOWN)
# Shutdown Application Add-ons, using Home Assistant API
await self.sys_addons.shutdown(AddonStartup.APPLICATION)
try:
# Shutdown Application Add-ons, using Home Assistant API
await self.sys_addons.shutdown(AddonStartup.APPLICATION)
# Close Home Assistant
with suppress(HassioError):
await self.sys_homeassistant.core.stop(
remove_container=remove_homeassistant_container
)
# Close Home Assistant
with suppress(HassioError):
await self.sys_homeassistant.core.stop(
remove_container=remove_homeassistant_container
)
# Shutdown System Add-ons
await self.sys_addons.shutdown(AddonStartup.SERVICES)
await self.sys_addons.shutdown(AddonStartup.SYSTEM)
await self.sys_addons.shutdown(AddonStartup.INITIALIZE)
# Shutdown System Add-ons
await self.sys_addons.shutdown(AddonStartup.SERVICES)
await self.sys_addons.shutdown(AddonStartup.SYSTEM)
await self.sys_addons.shutdown(AddonStartup.INITIALIZE)
# Shutdown all Plugins
if self.state in (CoreState.STOPPING, CoreState.SHUTDOWN):
await self.sys_plugins.shutdown()
# Shutdown all Plugins
if self.state in (CoreState.STOPPING, CoreState.SHUTDOWN):
await self.sys_plugins.shutdown()
finally:
self._shutdown_event.set()
async def _update_last_boot(self) -> None:
"""Update last boot time."""

View File

@@ -9,7 +9,7 @@ from typing import Self
from awesomeversion import AwesomeVersion
from ..const import BusEvent, CoreState
from ..const import BusEvent
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import (
DBusError,
@@ -233,14 +233,6 @@ class HostManager(CoreSysAttributes):
if not active:
continue
# Only handle if Supervisor didn't initiate the shutdown
if self.sys_core.state != CoreState.RUNNING:
_LOGGER.debug(
"PrepareForShutdown received but already in state %s",
self.sys_core.state,
)
break
_LOGGER.info(
"Host shutdown/reboot detected, gracefully stopping services"
)

View File

@@ -1,7 +1,7 @@
"""Test host manager."""
import asyncio
from unittest.mock import AsyncMock, patch
from unittest.mock import patch
from awesomeversion import AwesomeVersion
import pytest
@@ -109,22 +109,25 @@ async def test_host_shutdown_on_prepare_for_shutdown_signal(
await shutdown_called.wait()
async def test_host_shutdown_signal_ignored_when_not_running(
async def test_host_shutdown_signal_reentrant(
coresys: CoreSys, logind_service: LogindService
):
"""Test PrepareForShutdown is ignored if Supervisor already shutting down."""
"""Test PrepareForShutdown during in-progress shutdown awaits same shutdown."""
shutdown_called = asyncio.Event()
async def mock_shutdown(**kwargs):
shutdown_called.set()
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:
with patch.object(coresys.core, "shutdown", side_effect=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()
# shutdown() is called reentrantly - it awaits the in-progress shutdown
async with asyncio.timeout(2):
await shutdown_called.wait()