Take shutdown inhibitor lock for graceful service teardown on external shutdown#6631
Take shutdown inhibitor lock for graceful service teardown on external shutdown#6631
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds systemd-logind integration to gracefully stop Supervisor-managed services when the host is shutting down/rebooting, using a logind inhibitor lock and the PrepareForShutdown signal.
Changes:
- Add logind D-Bus support for
Inhibit()andPrepareForShutdownsignal handling. - Start a background shutdown-monitor task in
HostManagerto triggercoresys.core.shutdown()when host shutdown is detected. - Extend/adjust tests and D-Bus service mocks to cover the new behavior, and enable UNIX FD negotiation on the system D-Bus connection.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/host/test_manager.py | Adds end-to-end tests ensuring Supervisor shuts down (or ignores) when PrepareForShutdown is emitted. |
| tests/dbus/test_login.py | Adds tests for Logind.inhibit() and prepare_for_shutdown() signal wrapper behavior. |
| tests/dbus_service_mocks/logind.py | Extends logind mock with Inhibit (FD return) and PrepareForShutdown signal. |
| supervisor/host/manager.py | Introduces shutdown monitor task that takes a logind inhibitor lock and listens for host shutdown signal. |
| supervisor/dbus/manager.py | Enables UNIX FD negotiation on system D-Bus connection (needed for inhibitor FD passing). |
| supervisor/dbus/logind.py | Adds logind inhibit + signal wrapper API. |
| supervisor/dbus/const.py | Adds constant for logind PrepareForShutdown signal. |
| supervisor/core.py | Ensures HostManager unload() is invoked during shutdown. |
|
This is an overview of Supervisor shutdown/reboot. What is a bit counter intuitive is that Supervisor shutdown is really about all components (Core, plug-ins, apps) and a separate flow from Supervisor's own "shutdown", which is rather named stop. This code really adds the External (ACPI/hypervisor) via logind |
Handle external shutdown events (ACPI power button, hypervisor shutdown) by listening to logind's PrepareForShutdown signal. A delay inhibitor lock is acquired on startup so Supervisor has time to gracefully stop all managed services before the host proceeds with shutdown. Changes: - Add inhibit() and prepare_for_shutdown() methods to Logind D-Bus interface - Enable Unix FD negotiation on the D-Bus message bus for inhibitor lock FDs - Add background monitor task in HostManager that listens for the signal - Track the monitor task and cancel it cleanly via new unload() method - Wire host unload into Core.stop() stage 2 for clean shutdown - Add PrepareForShutdown signal constant to dbus/const.py Co-Authored-By: Claude Opus 4.6 <[email protected]>
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 <[email protected]>
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) <[email protected]>
If PrepareForShutdown fires while Supervisor is still starting up, wait for startup to complete before running the graceful shutdown sequence. This prevents shutting down partially initialized containers and services. The _startup_complete event is set automatically in set_state() whenever state transitions to RUNNING. If SIGTERM arrives during startup, core.stop() cancels the monitor task via host.unload(), cleanly interrupting the wait. Co-Authored-By: Claude Opus 4.6 <[email protected]>
c2feeef to
def618b
Compare
|
In my testing this works quite well, but there are some small gaps: When Supervisor updates or restarts, it calls core.stop() which terminates the process. The Docker container exits and is restarted by the I've then thought about using Which actually made me wonder, are we on the right track here? Maybe it would be better to react on |
Proposed change
When the host is shut down externally (e.g. ACPI power button, hypervisor shutdown command), Docker sends SIGTERM to Supervisor without any prior warning. This means managed containers (Home Assistant Core, add-ons, plugins) are killed abruptly without graceful shutdown.
This PR adds handling for logind's
PrepareForShutdownsignal so Supervisor can gracefully stop all services before the host proceeds with shutdown:delayinhibitor lock from logind viaInhibit()PrepareForShutdown(true)signalcore.shutdown()to gracefully stop all managed containersCore.stop()stage 2Key details:
negotiate_unix_fd=True) sinceInhibit()returns a file descriptorHostManager.unload()methodType of change
Additional information
Checklist
ruff format supervisor tests)If API endpoints or add-on configuration are added/changed: