fix(templates): forward the container stop to services supervised by ha_entrypoint (#3118)

* fix(templates): forward the container stop to services supervised by ha_entrypoint

When ha_entrypoint.sh is PID 1 (init: false add-ons on images without
s6-overlay: comicarr, komga, degoog, ...), terminate() signals only its
direct children. Those are the per-service restart-loop subshells, which
died on SIGTERM without passing it on; PID 1 then exited and the kernel
SIGKILLed every service. No service ever got a chance to shut down cleanly.

Each loop now runs its service (and the restart delay) in the background,
traps TERM/INT, forwards SIGTERM to that process, waits for it to exit, and
leaves the loop instead of restarting it. The "Termination signal received"
line the postgres add-ons sed into is unchanged.

Implementation drafted by Codex (gpt-5.6-sol), reviewed and tested here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(templates): stop the service's whole process group, and wait for it to empty

A run file that does not exec its daemon only had its wrapper shell
signalled: portainer_agent's own run file, and LinuxServer's svc-transmission
in base images we cannot edit. Each loop subshell now runs with job control
(set -m), so every service is its own process group; the stop is sent to the
group and PID 1 waits until the group is empty, so the daemon drains instead
of being SIGKILLed when PID 1 exits. Docker's stop timeout still bounds it.

Revision drafted by Codex (gpt-5.6-sol); the group-drain wait was added after
a PID-1 test showed the daemon got TERM but was killed mid-drain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexandreandClaude Opus 5.5 authored and GitHub committed 2026-10-14 06:58:19 +02:00
1 parent 92d2862e0e
commit eaa01c4eb0
1 file changed
+32 -3
+32 -3
View File
@@ -437,9 +437,34 @@ if $PID1; then
(
restart_count=0
max_restarts=5
# terminate() signals only PID 1's direct children, which are these loop subshells.
# Give each background job its own process group and forward the stop to the whole
# group, so wrappers and their daemons can shut down before PID 1 exits.
set -m
stopping=false
service_pid=
trap 'stopping=true; [ -z "$service_pid" ] || kill -TERM -- "-$service_pid" 2>/dev/null || true' TERM INT
wait_for_service() {
local pid="$service_pid"
if $stopping; then
kill -TERM -- "-$pid" 2>/dev/null || true
fi
_svc_rc=0
wait "$pid" 2>/dev/null || _svc_rc=$?
if $stopping; then
wait "$pid" 2>/dev/null || true
# A run file that does not exec its daemon dies first; wait for the whole group
while kill -0 -- "-$pid" 2>/dev/null; do sleep 0.2; done
service_pid=
return 1
fi
service_pid=
}
while true; do
_svc_rc=0
"$runfile" || _svc_rc=$?
"$runfile" &
service_pid=$!
wait_for_service || break
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
_svc_rc=0
@@ -448,7 +473,9 @@ if $PID1; then
. "$BASHIO_LIB" 2>/dev/null || true
# shellcheck disable=SC1090
. "$runfile"
) || _svc_rc=$?
) &
service_pid=$!
wait_for_service || break
fi
rc=$_svc_rc
if [ "$rc" -eq 0 ]; then
@@ -461,7 +488,9 @@ if $PID1; then
break
fi
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
sleep 5
sleep 5 &
service_pid=$!
wait_for_service || break
done
) &
done