2 Commits
Author SHA1 Message Date
magdevandClaude Opus 4.7 9f524104b9 plan: fix version-section ordering + bring v0.1.1 entry up to date
The v0.1.2 section had been prepended above v0.1.1 instead of inserted
after it; reads strictly chronologically now (v0.1.0 → v0.1.1 → v0.1.2
→ v0.2.0 → …).

Also brought v0.1.1's heading and bullet list up to current reality:

  - heading: "ready to tag" → "shipped 2026-05-03" (it was tagged
    earlier today)
  - added the cache-wipe-on-bundled-launch fix, which actually landed
    in v0.1.1 (rotated into the tag) but was missing from PLAN.md's
    summary (CHANGELOG already had it)

Top-of-file status line: "v0.1.1 ready to tag" → "v0.1.0 + v0.1.1
shipped 2026-05-03; v0.1.2 in progress on dev".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:52:20 +02:00
magdevandClaude Opus 4.7 f132c3c9b6 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>
2026-05-03 15:49:13 +02:00
4 changed files with 60 additions and 5 deletions

No files matched your search

+8 -1
View File
@@ -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) - (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 ## [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). 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. - 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. - 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.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 [0.1.0]: https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0
+8 -3
View File
@@ -1,6 +1,6 @@
# php-qml — Plan for a Symfony/FrankenPHP + Qt/QML Desktop Framework # php-qml — Plan for a Symfony/FrankenPHP + Qt/QML Desktop Framework
> **Status (2026-05):** v0.1.0 shipped 2026-05-03 (LGPL-3.0-or-later). v0.1.1 ready to tag — closes the four shakedown follow-ups. Planning is version-based — see §13. > **Status (2026-05):** v0.1.0 + v0.1.1 shipped 2026-05-03 (LGPL-3.0-or-later). v0.1.2 in progress on `dev`. Planning is version-based — see §13.
> >
> **Where else to look:** > **Where else to look:**
> >
@@ -531,14 +531,19 @@ 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 <https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0>. First public preview. Linux AppImage. Full entry in `CHANGELOG.md`; binaries at <https://src.bundespruefstelle.ch/magdev/php-qml/releases/tag/v0.1.0>.
### v0.1.1 — bugfix release (ready to tag) ### v0.1.1 — shipped 2026-05-03
All four shakedown follow-ups landed: Closed the four shakedown follow-ups identified during v0.1.0 shipping:
- **perfsmoke gap closed.** `HealthController` now constructor-injects `Publisher`; `/healthz` returns 200 only when `BridgeBundle` is fully container-resolvable. The `bundle` field in the response is the canary value perfsmoke + the bundled-mode integration test both check. - **perfsmoke gap closed.** `HealthController` now constructor-injects `Publisher`; `/healthz` returns 200 only when `BridgeBundle` is fully container-resolvable. The `bundle` field in the response is the canary value perfsmoke + the bundled-mode integration test both check.
- **Bundled-mode supervisor integration test.** `examples/todo/tests/bundled-supervisor.sh` (run via `make integration-bundled`) stages a fake AppImage layout in `/tmp` and exercises the whole supervisor codepath (`resolveFrankenphpBin` → `runMigrations` → `spawnChild` → cache/log redirect to user data dir) without needing a real `.AppImage` build. Wired into ci.yml. Catches every v0.1.0 shakedown bug. - **Bundled-mode supervisor integration test.** `examples/todo/tests/bundled-supervisor.sh` (run via `make integration-bundled`) stages a fake AppImage layout in `/tmp` and exercises the whole supervisor codepath (`resolveFrankenphpBin` → `runMigrations` → `spawnChild` → cache/log redirect to user data dir) without needing a real `.AppImage` build. Wired into ci.yml. Catches every v0.1.0 shakedown bug.
- **Skeleton AppImage parity.** `framework/skeleton/Makefile` gains `staging-symfony` + `appimage` targets mirroring the example's; `framework/skeleton/packaging/` ships starter `.desktop` + `.png`; `bin/php-qml-init` rewrites `BUNDLE_SRC` / `PACKAGING` Make variables and renames packaging files at scaffold time. `--vendor` mode also vendors `packaging/linux/` to `.bridge-packaging/`. Scaffolded apps inherit a working `make appimage` flow. - **Skeleton AppImage parity.** `framework/skeleton/Makefile` gains `staging-symfony` + `appimage` targets mirroring the example's; `framework/skeleton/packaging/` ships starter `.desktop` + `.png`; `bin/php-qml-init` rewrites `BUNDLE_SRC` / `PACKAGING` Make variables and renames packaging files at scaffold time. `--vendor` mode also vendors `packaging/linux/` to `.bridge-packaging/`. Scaffolded apps inherit a working `make appimage` flow.
- **Caddyfile fmt.** `framework/skeleton/Caddyfile` and `examples/todo/Caddyfile` reformatted per `caddy fmt`; the "Caddyfile input is not formatted" boot warning is gone. - **Caddyfile fmt.** `framework/skeleton/Caddyfile` and `examples/todo/Caddyfile` reformatted per `caddy fmt`; the "Caddyfile input is not formatted" boot warning is gone.
- **Cache-wipe on bundled launch** (added during v0.1.1 shakedown). Symfony bakes `kernel.project_dir` into its compiled cache; the AppImage's FUSE mount path changes per launch, so cache from launch N is stale by N+1. Supervisor now wipes `var/cache/` on every `initBundledMode`. Build-time cache warmup is the v0.2.0 follow-up.
### v0.1.2 — in progress 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.2.0 — next minor ### v0.2.0 — next minor
+35 -1
View File
@@ -186,4 +186,38 @@ done
echo "$HEALTHZ2_BODY" | grep -q '"status":"ok"' \ echo "$HEALTHZ2_BODY" | grep -q '"status":"ok"' \
|| fail "2nd-launch /healthz didn't return 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)."
+9
View File
@@ -42,6 +42,15 @@ BackendConnection::BackendConnection(QObject* parent)
m_retryTimer->setInterval(kProbeIntervalMs); m_retryTimer->setInterval(kProbeIntervalMs);
connect(m_retryTimer, &QTimer::timeout, this, &BackendConnection::probe); 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")); const QString explicitUrl = QString::fromUtf8(qgetenv("BRIDGE_URL"));
if (!explicitUrl.isEmpty()) { if (!explicitUrl.isEmpty()) {
m_mode = Mode::Dev; m_mode = Mode::Dev;