mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-20 08:44:00 +02:00
fix(templates): install the stop handler before startup, not after it (#3054)
* fix(templates): install the stop handler before startup, not after it
`.templates/ha_entrypoint.sh` installed its SIGTERM/SIGINT trap at the very
end of startup, after `wait_for_supervisor` and after the whole
`/etc/cont-init.d/*` loop. 105 of the 136 add-ons here set `init: false`,
which makes this script namespace PID 1, and the kernel discards a signal
whose handler is still SIG_DFL. A stop arriving during startup was therefore
lost outright: Home Assistant waited out the grace period, the container died
on SIGKILL, and the add-on surfaced as Error.
The trap now goes in immediately after the PID1 detection block, still behind
an `if $PID1` guard so the 31 add-ons running under Docker's own init keep
their current signal behaviour. `terminate()` itself is unchanged apart from
indentation.
Measured on this file, run as PID 1 in a PID+mount namespace with a
bind-mounted `/etc/cont-init.d`:
SIGTERM 3s into an 8s cont-init script
before: never exits (alive >40s) after: exits 5s later, script wrote 8/8
SIGTERM before cont-init starts
before: never exits (alive >40s) after: exits 8s later, script wrote 8/8
SIGTERM during 8 fast cont-init scripts
before: never exits (alive >20s) after: exits in 0s, cleanly after 6 of 8
Nothing is interrupted mid-write. Only PID 1 is signalled and bash defers a
trap to the next command boundary, so the in-flight command always reaps
first: a `dd` writing 16,384,000 bytes over ~5s took the entrypoint's SIGTERM
at 2s and still wrote all 16,384,000 bytes.
The residual is that a stop during one long-running cont-init script waits for
that script. Backgrounding the loop and killing the child would remove it, but
that truncates the child mid-write, and `01-config_yaml.sh` and
`19-json_repair.sh` both write non-atomically into the persisted `/config`.
Waiting was preferred over risking a user's config.
omni-tools is bumped so this gets a CI build and the fix reaches the add-on
the report came from; every other add-on picks it up on its next rebuild.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(templates): make terminate() errexit-safe before it can run early
Moving the trap to the top of the script exposed a latent hazard that only
existed once the handler could fire before the application was up.
`validate_shebang()` leaves `set -e` on in the outer shell, so `terminate()`
runs under errexit -- measured: the shell flags at the cont-init stage are
`ehB`. Under errexit the first failing command in a trap handler aborts the
handler and exits with that status, skipping the child-kill loop and the
`exit 0`.
Every command in the stock body is already guarded with `|| true`, `|| echo`
or an `if`, so this was inert. But postgres_15 and postgres_17 patch this
function at build time (both `Dockerfile:77`):
sed -i "/Termination signal received/a gosu postgres pg_ctl -D \"$PGDATA\" -m fast stop"
That command is unguarded, and it fails whenever the stop arrives before the
database is up. With the trap installed late, that never happened: postgres was
already running by the time the handler existed. With the trap installed early
the window is real -- it spans `wait_for_supervisor` (up to 30s) and the whole
cont-init chain, which for postgres_15 includes a `chown -R` over `$PGDATA`.
Replaying that exact sed against the patched entrypoint and stopping during
cont-init:
without set +e exit 127, handler aborted before killing any child
with set +e exit 0, handler ran to completion
Exiting non-zero on a stop is what Home Assistant reports as Error, so without
this the change would have reproduced the reported symptom for those two
add-ons in a narrower window.
`set +e` states the intent the guarded body already implies, and covers the
injected line without postgres_15/17 needing an edit. They are the only two
add-ons that patch terminate(); the other six Dockerfiles that sed the
entrypoint touch other regions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: BirdNET-Go Addon Builder <addon-builder@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,54 @@ else
|
||||
echo "Starting custom scripts"
|
||||
fi
|
||||
|
||||
##########################################
|
||||
# Install the stop handler #
|
||||
##########################################
|
||||
|
||||
# As namespace PID 1 -- which is what "init: false" makes this script -- the kernel
|
||||
# discards any signal whose handler is still SIG_DFL. Installed at the end of startup,
|
||||
# as it used to be, the handler missed every stop that arrived while the Supervisor
|
||||
# probe or the cont-init chain was still running: Home Assistant waited out the grace
|
||||
# period for a SIGKILL and reported the add-on as failed. Nothing here is interrupted
|
||||
# mid-write by the move -- only PID 1 is signalled, and bash runs a trap at a command
|
||||
# boundary, so whatever external command is in flight reaps first.
|
||||
terminate() {
|
||||
local local_pid
|
||||
# Best-effort, so errexit must not apply: this runs with `set -e` in force (validate_shebang
|
||||
# leaves it on), and under errexit the first failing command aborts the handler and exits with
|
||||
# its status, skipping the child-kill loop and the `exit 0` below. Every command in the body
|
||||
# here is already guarded, but add-ons patch this function at build time -- postgres_15 and
|
||||
# postgres_17 sed an unguarded `pg_ctl ... stop` in right after the echo -- and that command
|
||||
# fails whenever the stop arrives before the database is up.
|
||||
set +e
|
||||
echo "Termination signal received, forwarding to subprocesses..."
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
while read -r pid; do
|
||||
[ -n "$pid" ] || continue
|
||||
echo "Terminating child PID $pid"
|
||||
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
|
||||
done < <(pgrep -P "$$" || true)
|
||||
else
|
||||
for p in /proc/[0-9]*/; do
|
||||
local_pid="${p#/proc/}"
|
||||
local_pid="${local_pid%/}"
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
|
||||
echo "Terminating child PID $local_pid"
|
||||
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
wait || true
|
||||
echo "All subprocesses terminated. Exiting."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Only when this script is PID 1. Under Docker's own init the entrypoint is an ordinary
|
||||
# child and the signal handling above belongs to that init, not to us.
|
||||
if $PID1; then
|
||||
trap terminate SIGTERM SIGINT
|
||||
fi
|
||||
|
||||
##########################################
|
||||
# Pick an exec-capable directory #
|
||||
##########################################
|
||||
@@ -428,31 +476,6 @@ if $PID1; then
|
||||
echo " "
|
||||
echo -e "\033[0;32mEverything started!\033[0m"
|
||||
|
||||
terminate() {
|
||||
local local_pid
|
||||
echo "Termination signal received, forwarding to subprocesses..."
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
while read -r pid; do
|
||||
[ -n "$pid" ] || continue
|
||||
echo "Terminating child PID $pid"
|
||||
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
|
||||
done < <(pgrep -P "$$" || true)
|
||||
else
|
||||
for p in /proc/[0-9]*/; do
|
||||
local_pid="${p#/proc/}"
|
||||
local_pid="${local_pid%/}"
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
|
||||
echo "Terminating child PID $local_pid"
|
||||
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
wait || true
|
||||
echo "All subprocesses terminated. Exiting."
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap terminate SIGTERM SIGINT
|
||||
while :; do
|
||||
sleep infinity &
|
||||
wait $!
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
## 0.6.1.2 (2026-09-07)
|
||||
- Rebuild to pick up a shared `ha_entrypoint.sh` fix: the SIGTERM/SIGINT handler is now installed at the top of the entrypoint instead of at the end of startup. With `init: false` the entrypoint is namespace PID 1, and the kernel discards a signal that PID 1 has no handler for, so a stop arriving while the Supervisor probe or the `cont-init.d` chain was still running was lost entirely and the add-on died only when the grace period expired into SIGKILL. Stops are now honoured from the first moment of startup. No change to add-on behaviour otherwise.
|
||||
|
||||
## 0.6.1.1 (2026-09-07)
|
||||
- Fix the add-on refusing to stop: Home Assistant reported an Error status after a few seconds and the container kept running and serving the web UI. `cont-init.d/99-run.sh` started nginx in the foreground, and `ha_entrypoint.sh` runs every cont-init script in the foreground, so the entrypoint never reached the point where it installs the `terminate()` handler that forwards SIGTERM to the application on shutdown. The application is now started in the background, and `init: false` makes the entrypoint run as PID 1 so the orphaned process is reparented to it and receives that signal. Closes #3049.
|
||||
|
||||
|
||||
@@ -21,5 +21,5 @@ schema:
|
||||
value: str?
|
||||
slug: omni-tools
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: 0.6.1.1
|
||||
version: 0.6.1.2
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:80]"
|
||||
|
||||
Reference in New Issue
Block a user