diff --git a/CHANGELOG.md b/CHANGELOG.md index 079d2a6..69e4cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) - (none yet — next changes land here) +## [0.1.2] — TBD + +### Fixed + +- **Bundled supervisor: clean child shutdown.** `BackendConnection`'s destructor was the only path that called `teardownChild()`, but it ran during stack unwinding *after* `app.exec()` returned — by then the Qt event loop was already mid-shutdown and `QProcess::waitForFinished` couldn't reliably reap the child. Symptom: Qt logged `QProcess: Destroyed while process ("...frankenphp") is still running`, frankenphp + its PHP workers became orphans. The constructor now also connects `QCoreApplication::aboutToQuit` → `teardownChild`, so the child is SIGTERM'd while the event loop is still active. The bundled-supervisor integration test gained a clean-shutdown assertion (no Qt warning + no orphan frankenphp under the host's PGID after SIGTERM). + ## [0.1.1] — 2026-05-03 Bugfix release closing the four follow-ups identified during the v0.1.0 shakedown. No new public API surface; `/healthz` response gains an additive `bundle` field (existing JSON consumers ignore unknown keys). @@ -62,6 +68,7 @@ First public preview. Phases 0 through 4a in PLAN.md are complete plus the Phase - The bundle ships without `composer.lock` (it's a library); the skeleton and the todo example carry their own. - Licensed under **LGPL-3.0-or-later** (`LICENSE` + `LICENSE.GPL` at the repo root). Chosen to align with Qt 6's LGPLv3 licensing — see PLAN.md §12 for the relinkability obligations the AppImage build already honours. -[Unreleased]: https://src.bundespruefstelle.ch/magdev/php-qml/compare/v0.1.1...HEAD +[Unreleased]: https://src.bundespruefstelle.ch/magdev/php-qml/compare/v0.1.2...HEAD +[0.1.2]: https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.2 [0.1.1]: https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.1 [0.1.0]: https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0 diff --git a/PLAN.md b/PLAN.md index f7f2096..0685086 100644 --- a/PLAN.md +++ b/PLAN.md @@ -531,6 +531,12 @@ Per-phase scope detail is preserved in `CHANGELOG.md` (per-version summary) and First public preview. Linux AppImage. Full entry in `CHANGELOG.md`; binaries at . +### v0.1.2 — next bugfix + +In-flight on `dev`: + +- **Bundled supervisor: clean child shutdown.** The destructor's `teardownChild()` only ran during stack unwinding *after* `app.exec()` returned, by which point Qt's event loop was already mid-shutdown — so `QProcess::waitForFinished` couldn't reliably reap the child and Qt warned `QProcess: Destroyed while process is still running`, leaving an orphan frankenphp + its workers behind. Fix: connect `QCoreApplication::aboutToQuit` to `teardownChild` in the constructor, so the child is SIGTERM'd while the event loop is still active. Bundled-supervisor integration test gained a clean-shutdown assertion (no Qt warning, no orphan frankenphp under the host's PGID after SIGTERM). + ### v0.1.1 — bugfix release (ready to tag) All four shakedown follow-ups landed: diff --git a/examples/todo/tests/bundled-supervisor.sh b/examples/todo/tests/bundled-supervisor.sh index a55affc..06b215c 100755 --- a/examples/todo/tests/bundled-supervisor.sh +++ b/examples/todo/tests/bundled-supervisor.sh @@ -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)." diff --git a/framework/qml/src/BackendConnection.cpp b/framework/qml/src/BackendConnection.cpp index 44f0d88..7982f6a 100644 --- a/framework/qml/src/BackendConnection.cpp +++ b/framework/qml/src/BackendConnection.cpp @@ -42,6 +42,15 @@ BackendConnection::BackendConnection(QObject* parent) m_retryTimer->setInterval(kProbeIntervalMs); connect(m_retryTimer, &QTimer::timeout, this, &BackendConnection::probe); + // aboutToQuit fires while the event loop is still active, before main() + // starts unwinding the stack. Without this, teardownChild only runs from + // ~BackendConnection — by then the QQmlEngine is already mid-destruction + // and Qt warns "QProcess: Destroyed while process is still running". + if (QCoreApplication::instance()) { + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, + this, &BackendConnection::teardownChild); + } + const QString explicitUrl = QString::fromUtf8(qgetenv("BRIDGE_URL")); if (!explicitUrl.isEmpty()) { m_mode = Mode::Dev;