Files
php-qml/CHANGELOG.md
magdev 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

8.7 KiB

Changelog

All notable changes to this project are documented here.

The format is based on Keep a Changelog and this project adheres to Semantic Versioning. Pre-v1.0.0, minor bumps may break public API.

Unreleased

Added

  • (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::aboutToQuitteardownChild, 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).

Fixed

  • Wipe Symfony cache on bundled-mode launch. Symfony's compiled container bakes kernel.project_dir as an absolute path. In bundled mode that path lives inside the AppImage's FUSE mount (/tmp/.mount_<random>), which is regenerated every launch. So the cache from launch N referenced mount-N's path; launch N+1 (different mount) hit InvalidDirectory from doctrine-migrations on the first launch-2 (and similar at any kernel.project_dir-sensitive lookup). BackendConnection::initBundledMode now rmdirs the cache before each spawn. Costs ~1-2s of warmup per launch; build-time cache warmup is the permanent fix (PLAN.md §13 v0.2.0). The bundled-supervisor integration test gained a 2nd-launch-from-fresh-staging step so this regresses if forgotten.
  • HealthController deep-loads the bundle. Constructor-injects Publisher so /healthz returns 200 only when BridgeBundle is fully container-resolvable. v0.1.0's /healthz returned 200 against half-loaded bundles — both the path-repo symlink dangling at runtime and the read-only-cache failure shipped green through perfsmoke as a result. Response body now includes bundle: "PhpQml\\Bridge\\Publisher" as the canary value.
  • Caddyfile formatting. framework/skeleton/Caddyfile and examples/todo/Caddyfile reformatted with caddy fmt. The "Caddyfile input is not formatted; run 'caddy fmt --overwrite'" warning that fired on every FrankenPHP boot is gone.

Added

  • Bundled-mode supervisor integration test (examples/todo/tests/bundled-supervisor.sh, make integration-bundled). Stages a fake AppImage layout in /tmp (host binary copied — Qt's applicationDirPath() dereferences symlinks via /proc/self/exe, so the real layout has to be mimicked closely; staged Symfony tree is chmod -R a-w to actually exercise the read-only-mount cache redirect) and exercises the supervisor end-to-end without needing a real .AppImage build. Asserts /healthz deep-load + cache redirect. Wired into .gitea/workflows/ci.yml after the existing dev-mode integration test.
  • Skeleton AppImage parity. framework/skeleton/Makefile gains staging-symfony + appimage targets mirroring examples/todo/Makefile's. New framework/skeleton/packaging/skeleton.{desktop,png} provide minimal AppImage assembly inputs. bin/php-qml-init now: (a) renames packaging files to match the scaffolded app name, (b) rewrites the .desktop file's Name/Exec/Icon, (c) substitutes the new BUNDLE_SRC and PACKAGING Makefile variables to either absolute framework paths (default) or vendored .bridge / .bridge-packaging paths (--vendor). Scaffolded apps inherit make appimage working out of the box.

Notes

  • BackendConnection::m_port stays hardcoded to 8765 — port-collision between two installed php-qml apps is a real bug surfaced during v0.1.1 prep, but the fix touches every consumer that hardcodes 8765 (perfsmoke, the new bundled-supervisor test) so it's tracked as a v0.2.0 item rather than a v0.1.x bugfix.

0.1.0 — 2026-05-03

First public preview. Phases 0 through 4a in PLAN.md are complete plus the Phase 5 DX-polish sub-commits. Linux is the only packaged target; macOS and Windows are deferred to 4b / 4c. Tagging is the user's call (release CI runs on v* tags).

Added

  • Process-pair architecture. Qt/QML host owns rendering; bundled FrankenPHP child runs the Symfony app in worker mode. They communicate via local HTTP and Mercure SSE.
  • Symfony bundle (php-qml/bridge). BridgeBundle wires Doctrine subscriber, ModelPublisher, bridge:doctor console command, and the #[BridgeResource] attribute so app code stays idiomatic Symfony.
  • Qt module (PhpQml.Bridge). BackendConnection (lifecycle + Update Semantics state machine: Connecting / Online / Reconnecting / Offline), RestClient, MercureClient, ReactiveListModel, ReactiveObject, AppShell, SingleInstance (QLocalServer-backed lock with launch-arg forwarding), DevConsole.
  • Update Semantics. Optimistic mutations with Idempotency-Key round-tripped to Mercure as correlationKey; in-flight pending role; offline overlay + reconnecting banner via AppShell.
  • Headline makers (symfony/maker-bundle):
    • make:bridge:resource <Name> — entity (#[BridgeResource] + UUIDv7 by default, --int-id for auto-increment), CRUD controller, starter <Name>List.qml.
    • make:bridge:command <Name> — controller stub for non-CRUD endpoints.
    • make:bridge:window <Name> — second-window QML scaffold.
  • Skeleton application (framework/skeleton) — minimal reference app exercised by every CI job.
  • POC todo app (examples/todo) — full list UI, multi-window mirror, mark-all-done command, end-to-end test of multi-window coherence and crash-recovery.
  • bundled mode. When BRIDGE_URL is unset (typical AppImage case), BackendConnection spawns the embedded FrankenPHP, generates a per-session bearer token, runs first-launch migrations into ~/.local/share/<app>/var/data.sqlite, and supervises the child with prctl(PR_SET_PDEATHSIG, SIGTERM) for cleanup safety.
  • Linux AppImage packaging. packaging/linux/build-appimage.sh + make appimage produce a single ~150 MB binary (Qt + Symfony + FrankenPHP + AppImageUpdate sidecar).
  • AppImageUpdate auto-update. Embedded update-info ELF section points at the canonical Gitea Releases URL. BackendConnection.checkForUpdates() / applyUpdate() invoke the bundled sidecar.
  • Release CI (.gitea/workflows/release.yml). Triggers on v* tags. Builds the AppImage, runs tests/perfsmoke.sh against PLAN.md §11 budgets (bundle ≤ 200 MB, cold start ≤ 4 s on shared CI runners, idle RSS ≤ 200 MB), generates zsync metadata + latest.json appcast + SHA256SUMS, optionally GPG-signs them, and uploads everything to the Gitea Release.
  • Quality CI (.gitea/workflows/ci.yml). PHPStan + php-cs-fixer (check) + PHPUnit + qmllint + maker snapshot test + bridge-integration test (HTTP/SSE round-trip + crash-recover) on every push to main.
  • DX polish (Phase 5):
    • DevConsole.qml — opt-in window into the bundled FrankenPHP child's merged stdout/stderr; 500-line ring buffer; `Ctrl+`` toggles it in skeleton + todo example.
    • bin/php-qml-init <name> — bash scaffolder. Copies framework/skeleton/, rewrites identifiers, repoints the path-composer-repo and CMake add_subdirectory(framework/qml) reference, runs composer install and migrations. --vendor produces a portable copy.
    • .vscode/launch.json + tasks.json + settings.json and .idea/runConfigurations/ shipped with skeleton and todo example.
    • Hot-reload story documented end-to-end (FrankenPHP --watch, Qt Creator Reload, qmlls live preview).

Notes

  • Tooling versions enforced: PHP 8.4+, Symfony 8, Doctrine ORM 3, Qt 6.5+, FrankenPHP 1.12.2.
  • 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.