diff --git a/.templates/ha_entrypoint.sh b/.templates/ha_entrypoint.sh index b8e1fd34e5..0f02dd1ade 100755 --- a/.templates/ha_entrypoint.sh +++ b/.templates/ha_entrypoint.sh @@ -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 $! diff --git a/omni-tools/CHANGELOG.md b/omni-tools/CHANGELOG.md index 84876f01dc..514f02cd07 100644 --- a/omni-tools/CHANGELOG.md +++ b/omni-tools/CHANGELOG.md @@ -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. diff --git a/omni-tools/config.yaml b/omni-tools/config.yaml index c837256c08..1a9ca5f8e8 100644 --- a/omni-tools/config.yaml +++ b/omni-tools/config.yaml @@ -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]"