1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-07-04 12:25:02 +01:00
Files
supervisor/tests/docker/test_monitor.py
T
Stefan Agner 7ecfe42602 apps: log container exit code when app exits non-zero (#6848)
* apps: log container exit code when app exits non-zero

Issue #6840 reports that stopping an app whose process exits 143 (SIGTERM
default disposition) leaves the app in AppState.ERROR. ERROR is the right
state for that — Docker itself treats any non-zero exit as a failure
(e.g. `--restart on-failure`), and 143 specifically means the SIGTERM
grace period was wasted because the app never installed a handler. But
Supervisor previously logged nothing about it, leaving authors with no
hint that their image is misbehaving.

Plumb the exit code through DockerContainerStateEvent and log it from
App.container_state_changed on transitions to FAILED: a warning for 143
nudging the author to trap SIGTERM and exit 0, and an error for any
other non-zero code (crashes, SIGKILL after grace, app's own error
exit).

Refactor _container_state_from_model to return (state, exit_code) so
the docker event monitor and DockerInterface.attach feed the same exit
code through one code path instead of re-reading State.ExitCode in the
caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* apps: address review feedback on exit-code logging

- Replace bare 143 with EXIT_CODE_SIGTERM_DEFAULT (128 + signal.SIGTERM)
  in supervisor/docker/const.py so the reasoning is documented in code,
  not just in the log string.
- Stop populating exit_code on STOPPED transitions. Previously the
  refactor made DockerInterface.attach emit exit_code=0 for cleanly
  stopped containers, while the monitor only emitted an exit code for
  abnormal exits. Align both paths so exit_code is only set on FAILED.
- Add test_app_failed_logs_exit_code covering the three new branches
  (warning on 143, error on other non-zero, silent when None) and
  extend test_attach_existing_container to assert the event's exit_code
  field per state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docker/monitor: flatten exit_code branch to satisfy pylint

The previous if/else inside the `die` branch pushed the function over
pylint's too-many-nested-blocks threshold (6/5). Collapse it back into
a pair of conditional expressions: container_state via ternary on the
exit code, exit_code via `die_exit_code or None` so 0 stays None.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update supervisor/apps/app.py

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2026-05-20 09:40:06 +02:00

155 lines
4.5 KiB
Python

"""Test docker events monitor."""
import asyncio
from typing import Any
from unittest.mock import patch
from aiodocker.containers import DockerContainer
from awesomeversion import AwesomeVersion
import pytest
from supervisor.bus import Bus
from supervisor.const import BusEvent
from supervisor.coresys import CoreSys
from supervisor.docker.const import ContainerState
from supervisor.docker.monitor import DockerContainerStateEvent
@pytest.mark.parametrize(
"event,expected,expected_exit_code",
[
(
{
"Type": "container",
"Action": "start",
"Actor": {"Attributes": {"supervisor_managed": ""}},
},
ContainerState.RUNNING,
None,
),
(
{
"Type": "container",
"Action": "die",
"Actor": {"Attributes": {"supervisor_managed": "", "exitCode": "0"}},
},
ContainerState.STOPPED,
None,
),
(
{
"Type": "container",
"Action": "die",
"Actor": {"Attributes": {"supervisor_managed": "", "exitCode": "137"}},
},
ContainerState.FAILED,
137,
),
(
{
"Type": "container",
"Action": "health_status: healthy",
"Actor": {"Attributes": {"supervisor_managed": ""}},
},
ContainerState.HEALTHY,
None,
),
(
{
"Type": "container",
"Action": "health_status: unhealthy",
"Actor": {"Attributes": {"supervisor_managed": ""}},
},
ContainerState.UNHEALTHY,
None,
),
(
{
"Type": "container",
"Action": "exec_die",
"Actor": {"Attributes": {"supervisor_managed": ""}},
},
None,
None,
),
(
{
"Type": "container",
"Action": "start",
"Actor": {"Attributes": {}},
},
None,
None,
),
(
{
"Type": "network",
"Action": "start",
"Actor": {"Attributes": {}},
},
None,
None,
),
],
)
async def test_events(
coresys: CoreSys,
event: dict[str, Any],
expected: ContainerState | None,
expected_exit_code: int | None,
):
"""Test events created from docker events."""
event["Actor"]["Attributes"]["name"] = "some_container"
event["Actor"]["ID"] = "abc123"
event["time"] = 123
with patch.object(
Bus, "fire_event", return_value=[coresys.create_task(asyncio.sleep(0))]
) as fire_event:
await coresys.docker.docker.events.channel.publish(event)
await asyncio.sleep(0)
await coresys.docker.monitor.unload()
if expected:
fire_event.assert_called_once_with(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
"some_container", expected, "abc123", 123, expected_exit_code
),
)
else:
fire_event.assert_not_called()
async def test_unlabeled_container(coresys: CoreSys, container: DockerContainer):
"""Test attaching to unlabeled container is still watched."""
container.id = "abc123"
container.show.return_value = {
"Name": "homeassistant",
"Id": "abc123",
"State": {"Status": "running"},
"Config": {},
}
await coresys.homeassistant.core.instance.attach(AwesomeVersion("2022.7.3"))
with patch.object(
Bus, "fire_event", return_value=[coresys.create_task(asyncio.sleep(0))]
) as fire_event:
await coresys.docker.docker.events.channel.publish(
{
"time": 123,
"Type": "container",
"Action": "die",
"Actor": {
"ID": "abc123",
"Attributes": {"name": "homeassistant", "exitCode": "137"},
},
}
)
await coresys.docker.monitor.unload()
fire_event.assert_called_once_with(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
"homeassistant", ContainerState.FAILED, "abc123", 123, 137
),
)