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:
@@ -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."""
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user