mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-05-02 22:30:59 +01:00
During system shutdown (reboot/poweroff), the watchdog was incorrectly detecting the Home Assistant Core container as failed and attempting to restart it. This occurred because Docker was stopping all containers in parallel with Supervisor's own shutdown sequence, causing the watchdog to trigger while add-ons were still being stopped. This led to an abrupt termination of Core before it could cleanly shut down its SQLite database, resulting in a warning on the next startup: "The system could not validate that the sqlite3 database was shutdown cleanly". The fix registers a supervisor state change listener that unregisters the watchdog when entering any shutdown state (SHUTDOWN, STOPPING, or CLOSE). This prevents restart attempts during both user-initiated reboots (via API) and external shutdown signals (Docker SIGTERM, console reboot commands). Since SHUTDOWN, STOPPING, and CLOSE are terminal states with no reverse transition back to RUNNING, no re-registration logic is needed. Fixes #6511 Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
248 lines
8.5 KiB
Python
248 lines
8.5 KiB
Python
"""Test Home Assistant watchdog."""
|
|
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, PropertyMock, patch
|
|
|
|
from aiodocker.containers import DockerContainer
|
|
from awesomeversion import AwesomeVersion
|
|
|
|
from supervisor.const import BusEvent, CoreState
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.docker.const import ContainerState
|
|
from supervisor.docker.monitor import DockerContainerStateEvent
|
|
from supervisor.exceptions import HomeAssistantError
|
|
|
|
|
|
async def test_home_assistant_watchdog(coresys: CoreSys) -> None:
|
|
"""Test homeassistant watchdog works correctly."""
|
|
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
|
|
with (
|
|
patch(
|
|
"supervisor.docker.interface.DockerInterface.version",
|
|
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
|
|
),
|
|
patch.object(type(coresys.homeassistant.core.instance), "attach"),
|
|
):
|
|
await coresys.homeassistant.core.load()
|
|
|
|
coresys.homeassistant.core.watchdog = True
|
|
|
|
with (
|
|
patch.object(type(coresys.homeassistant.core), "restart") as restart,
|
|
patch.object(type(coresys.homeassistant.core), "start") as start,
|
|
patch.object(
|
|
type(coresys.homeassistant.core.instance), "current_state"
|
|
) as current_state,
|
|
):
|
|
current_state.return_value = ContainerState.UNHEALTHY
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="homeassistant",
|
|
state=ContainerState.UNHEALTHY,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
restart.assert_called_once()
|
|
start.assert_not_called()
|
|
|
|
restart.reset_mock()
|
|
current_state.return_value = ContainerState.FAILED
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="homeassistant",
|
|
state=ContainerState.FAILED,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
restart.assert_not_called()
|
|
start.assert_called_once()
|
|
|
|
start.reset_mock()
|
|
# Do not process event if container state has changed since fired
|
|
current_state.return_value = ContainerState.HEALTHY
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="homeassistant",
|
|
state=ContainerState.FAILED,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
restart.assert_not_called()
|
|
start.assert_not_called()
|
|
|
|
# Do not restart when home assistant stopped normally
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="homeassistant",
|
|
state=ContainerState.STOPPED,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
restart.assert_not_called()
|
|
start.assert_not_called()
|
|
|
|
# Other containers ignored
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="addon_local_other",
|
|
state=ContainerState.UNHEALTHY,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
restart.assert_not_called()
|
|
start.assert_not_called()
|
|
|
|
|
|
async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> None:
|
|
"""Test home assistant watchdog rebuilds if start fails."""
|
|
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
|
|
with (
|
|
patch(
|
|
"supervisor.docker.interface.DockerInterface.version",
|
|
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
|
|
),
|
|
patch.object(type(coresys.homeassistant.core.instance), "attach"),
|
|
):
|
|
await coresys.homeassistant.core.load()
|
|
|
|
coresys.homeassistant.core.watchdog = True
|
|
|
|
with (
|
|
patch.object(
|
|
type(coresys.homeassistant.core), "start", side_effect=HomeAssistantError()
|
|
) as start,
|
|
patch.object(type(coresys.homeassistant.core), "rebuild") as rebuild,
|
|
patch.object(
|
|
type(coresys.homeassistant.core.instance),
|
|
"current_state",
|
|
return_value=ContainerState.FAILED,
|
|
),
|
|
):
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="homeassistant",
|
|
state=ContainerState.FAILED,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0.1)
|
|
start.assert_called_once()
|
|
rebuild.assert_called_once()
|
|
|
|
|
|
async def test_home_assistant_watchdog_skip_on_load(
|
|
coresys: CoreSys, container: DockerContainer
|
|
) -> None:
|
|
"""Test home assistant watchdog skips a crash event on load."""
|
|
container.show.return_value["State"]["Status"] = "stopped"
|
|
container.show.return_value["State"]["Running"] = False
|
|
container.show.return_value["State"]["ExitCode"] = 1
|
|
coresys.homeassistant.core.watchdog = True
|
|
|
|
events = AsyncMock()
|
|
coresys.bus.register_event(BusEvent.DOCKER_CONTAINER_STATE_CHANGE, events)
|
|
|
|
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
|
|
with (
|
|
patch(
|
|
"supervisor.docker.interface.DockerInterface.version",
|
|
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
|
|
),
|
|
patch.object(type(coresys.homeassistant.core), "restart") as restart,
|
|
patch.object(type(coresys.homeassistant.core), "start") as start,
|
|
):
|
|
await coresys.homeassistant.core.load()
|
|
|
|
# No events should be raised on attach
|
|
await asyncio.sleep(0)
|
|
events.assert_not_called()
|
|
restart.assert_not_called()
|
|
start.assert_not_called()
|
|
|
|
|
|
async def test_home_assistant_watchdog_unregisters_on_shutdown(
|
|
coresys: CoreSys,
|
|
) -> None:
|
|
"""Test home assistant watchdog unregisters when entering shutdown states."""
|
|
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
|
|
with (
|
|
patch(
|
|
"supervisor.docker.interface.DockerInterface.version",
|
|
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
|
|
),
|
|
patch.object(type(coresys.homeassistant.core.instance), "attach"),
|
|
):
|
|
await coresys.homeassistant.core.load()
|
|
|
|
coresys.homeassistant.core.watchdog = True
|
|
|
|
# Verify watchdog listener is registered
|
|
assert coresys.homeassistant.core._watchdog_listener is not None
|
|
watchdog_listener = coresys.homeassistant.core._watchdog_listener
|
|
|
|
with (
|
|
patch.object(type(coresys.homeassistant.core), "restart") as restart,
|
|
patch.object(type(coresys.homeassistant.core), "start") as start,
|
|
patch.object(
|
|
type(coresys.homeassistant.core.instance),
|
|
"current_state",
|
|
return_value=ContainerState.FAILED,
|
|
),
|
|
):
|
|
# Watchdog should respond to events before shutdown
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="homeassistant",
|
|
state=ContainerState.FAILED,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
start.assert_called_once()
|
|
start.reset_mock()
|
|
|
|
# Test each shutdown state
|
|
for shutdown_state in (CoreState.SHUTDOWN, CoreState.STOPPING, CoreState.CLOSE):
|
|
# Reload to reset listener
|
|
coresys.homeassistant.core._watchdog_listener = watchdog_listener
|
|
|
|
# Fire shutdown state change
|
|
coresys.bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, shutdown_state)
|
|
await asyncio.sleep(0)
|
|
|
|
# Verify watchdog listener is unregistered
|
|
assert coresys.homeassistant.core._watchdog_listener is None
|
|
|
|
# Watchdog should not respond to events after shutdown
|
|
coresys.bus.fire_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
|
|
DockerContainerStateEvent(
|
|
name="homeassistant",
|
|
state=ContainerState.FAILED,
|
|
id="abc123",
|
|
time=1,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
start.assert_not_called()
|
|
restart.assert_not_called()
|