bundled: SIGTERM the frankenphp child via aboutToQuit, not just the dtor

Symptom (user report on v0.1.1):

  QProcess: Destroyed while process ("/tmp/.mount_Todo-xbNoHHL/usr/bin/frankenphp") is still running.

…and the frankenphp child + its PHP workers were left orphaned after
the host exited.

Cause: teardownChild() was only called from ~BackendConnection. By
the time that destructor runs, app.exec() has already returned,
QQmlApplicationEngine is mid-destruction, and Qt's event loop is
half-torn-down. waitForFinished() doesn't reliably reap the child in
that window — QProcess gets destroyed by the QObject parent-chain
cleanup before the kernel reports the child as exited.

Fix: in BackendConnection's constructor, connect
QCoreApplication::aboutToQuit → teardownChild. aboutToQuit fires
while the event loop is still active and BEFORE main() starts
unwinding the stack, so SIGTERM + waitForFinished can do their job
properly. The destructor's teardownChild call stays as belt-and-
suspenders (no-op once aboutToQuit has already cleaned up — the
function is idempotent via the m_child = nullptr at its end).

The connect happens unconditionally in the constructor (not just for
bundled mode) because m_child is also nullptr in dev mode and
teardownChild handles that with its leading `if (!m_child) return;`.

Regression guard: examples/todo/tests/bundled-supervisor.sh gains a
"graceful shutdown" step:

  - Snapshots the host's child PIDs before SIGTERM
  - SIGTERMs the host, waits up to 3s for clean exit
  - Greps the host log for "QProcess: Destroyed while" — fail if found
  - Iterates the snapshotted PIDs, fails on any frankenphp orphan still alive

Verified locally: real AppImage + the integration test both clean up
without Qt warnings or orphan processes.

PLAN.md: new v0.1.2 section above v0.1.1, this is its first entry.
CHANGELOG.md: [0.1.2] — TBD section with the same Fixed entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 15:49:13 +02:00
parent 597e74edcf
commit f132c3c9b6
4 changed files with 58 additions and 2 deletions

View File

@@ -186,4 +186,38 @@ done
echo "$HEALTHZ2_BODY" | grep -q '"status":"ok"' \
|| fail "2nd-launch /healthz didn't return status:ok"
step "All bundled-supervisor assertions passed (incl. 2nd-launch cache wipe)."
# ── Clean shutdown: SIGTERM the host, assert no Qt warning + no orphan frankenphp ──
step "graceful shutdown — assert the supervisor kills its frankenphp child"
SHUTDOWN_PID="$PID"
# Capture every descendant PID before killing, so we can verify they all exit.
DESCENDANTS="$(pgrep -P "$SHUTDOWN_PID" || true)"
kill -TERM "$SHUTDOWN_PID" 2>/dev/null || true
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
kill -0 "$SHUTDOWN_PID" 2>/dev/null || break
sleep 0.2
done
if kill -0 "$SHUTDOWN_PID" 2>/dev/null; then
kill -KILL "$SHUTDOWN_PID" 2>/dev/null || true
fail "host didn't exit within 3s of SIGTERM"
fi
PID=""
# Qt warning means QProcess was destroyed before the child exited.
if grep -q "QProcess: Destroyed while process .* is still running" "$LOG2"; then
sed 's/^/ /' "$LOG2" >&2
fail "host exited but logged QProcess-destroyed-while-running warning"
fi
# Any descendant still alive = orphan; the supervisor's teardown didn't wait.
for d in $DESCENDANTS; do
if kill -0 "$d" 2>/dev/null; then
# Be specific: only frankenphp orphans matter (QtNetwork might leave
# short-lived helper threads but those exit on their own).
if ps -p "$d" -o comm= 2>/dev/null | grep -q frankenphp; then
kill -KILL "$d" 2>/dev/null || true
fail "frankenphp child PID $d outlived the host (supervisor didn't clean up)"
fi
fi
done
step "All bundled-supervisor assertions passed (incl. 2nd-launch cache wipe + clean shutdown)."