Commit Graph

5 Commits

Author SHA1 Message Date
0cca0785c0 v0.2.0 (2/N): HealthController deep-load canary → BridgeBundleInfo VO
Decouples /healthz from the publisher contract. v0.1.1 wired
HealthController to constructor-inject Publisher purely as a "is the
bundle resolvable" probe — that worked but cemented the publisher's
API as a readiness-test dependency, which was awkward once
PublisherInterface landed in v0.2.0 chunk 1.

Replace with BridgeBundleInfo: a tiny readonly VO carrying the
bundle's name + class FQCN. HealthController depends on this instead.
Same deep-load semantics (broken bundle → can't construct the VO →
500 on /healthz), no leaky publisher dep.

/healthz response shape:
  - `bundle`: was `PhpQml\\Bridge\\Publisher`,
              now `PhpQml\\Bridge\\BridgeBundle`
  - `name`:   new field, reports `php-qml/bridge`

bundled-supervisor.sh's grep updated to match the new canary value
plus an assertion that the new `name` field is present (catches a
botched BridgeBundleInfo wire-up that the bundle-class-name assertion
alone would miss).

Quality + maker snapshot + bundled-supervisor integration test all
pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:57:52 +02:00
ed4db00a62 bundled: relay SIGTERM/SIGINT into Qt's quit() via self-pipe
Some checks failed
CI / Quality (push) Failing after 5m46s
Release / Linux AppImage (push) Successful in 6m15s
The aboutToQuit-based teardown wired in v0.1.2 only fires when something
calls QCoreApplication::quit() — typically a window close. `kill -TERM`
to the host process bypasses Qt entirely (no default SIGTERM handler),
so teardownChild never ran on signal-driven shutdown. Local tests
passed on lucky timing because PR_SET_PDEATHSIG made the kernel SIGTERM
frankenphp once the host died, but the timing was racy and surfaced on
CI as "frankenphp child PID outlived the host (supervisor didn't clean
up)".

Fix: install a SIGTERM/SIGINT handler in BackendConnection that uses
the self-pipe pattern — the C signal handler writes one byte (the only
truly async-signal-safe primitive), a QSocketNotifier on the read end
calls QCoreApplication::quit() in the main thread, and aboutToQuit runs
the existing teardownChild before app.exec() returns. The host now
exits cleanly under `kill -TERM` from service managers, launchers, and
the test harness.

Also bumps the bundled-supervisor.sh first-relaunch grace from 2s to
3s — teardownChild itself waits up to 2s for frankenphp to finish after
SIGTERM, so the host needs ~2.x seconds to exit. The graceful-shutdown
step further down was already at 3s.

No public-API change; production-correctness fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:55:23 +02:00
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
597e74edcf bundled: wipe Symfony cache on every launch — mount path bakes into cache
All checks were successful
CI / Quality (push) Successful in 5m43s
Release / Linux AppImage (push) Successful in 6m13s
Reproduces with the v0.1.1 AppImage on the second launch (same user
data dir, fresh AppImage mount):

  phpqml.bridge.bundled: symfony:    "/tmp/.mount_Todo-xllnOHH/..."
  Cannot load migrations from "/tmp/.mount_Todo-xDBkOfG/.../migrations"
                                                  ^^^^^^^
                            stale path from PREVIOUS launch's cache

Symfony compiles `kernel.project_dir` (an absolute path) into its
cached container under var/cache/. We redirect var/cache into the
user data dir for read-only-mount survival (v0.1.0 fix), but the
*content* of that cache references the mount path that was active
when the cache was built. Next launch gets a different
/tmp/.mount_<random>; the cached refs are stale; first
project_dir-sensitive lookup blows up (doctrine migrations was the
canary; would also surface as misrouted assets, broken Twig template
paths, etc.).

Fix: BackendConnection::initBundledMode does
QDir(cacheDir).removeRecursively() right after creating the dirs but
before runMigrations spawns the doctrine subprocess. Symfony rebuilds
the cache against the current mount on every launch. Cost: ~1-2s of
warmup per cold start.

Permanent fix is build-time cache warmup (ship the prod cache inside
the AppImage, copy to user data dir on first launch, no per-launch
warmup) — already tracked as a v0.2.0 item in PLAN.md §13. v0.1.1
takes the simpler always-wipe approach since it's bugfix-class.

Regression guard: examples/todo/tests/bundled-supervisor.sh gains a
"2nd launch from fresh staging" step that tears down the first host,
re-stages a fresh fake AppImage layout (different /tmp dir = different
"mount path" from BackendConnection's perspective), and asserts
/healthz comes back up. Without the cache wipe, that step would fail
exactly the way doctrine did in the user's report.

Verified locally:
  - bundled-supervisor.sh passes (incl. 2nd-launch step)
  - Real AppImage: two consecutive launches both reach
    "phpqml.bridge.bundled: migrations OK" + frankenphp spawn

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:23:30 +02:00
813b064cc1 test: bundled-mode supervisor integration test (faked AppImage layout)
Stages a fake AppImage layout in /tmp without a real .AppImage build:
  $ROOT/usr/bin/<app>           — copy of the host binary
  $ROOT/usr/bin/frankenphp      — symlink to system frankenphp
  $ROOT/usr/share/<app>/symfony — staged --no-dev composer copy
  $ROOT/usr/share/<app>/Caddyfile

The staged Symfony tree is `chmod -R a-w` to actually exercise the
read-only-mount cache/log redirect (Kernel::getCacheDir +
APP_CACHE_DIR override) — without the override, Symfony would fail
to mkdir var/cache/prod and migrations would error out.

Then runs the host with BRIDGE_URL unset (forces bundled mode), polls
/healthz, and asserts:

  - status=ok + bundle="PhpQml\Bridge\Publisher" — proves the
    HealthController deep-load (predecessor commit) actually
    autowired Publisher, i.e. BridgeBundle is reachable.
  - User data dir's var/cache exists — APP_CACHE_DIR override fired.
  - Staged tree's var/cache/prod is empty — Symfony didn't write into
    the read-only mount.

Together this catches every v0.1.0 shakedown bug in CI:
  - doubled bin/frankenphp path (resolveFrankenphpBin)
  - composer path-repo symlink dangling (staging-symfony's symlink:false sed)
  - read-only mount cache failure (Kernel + supervisor env-vars)
  - bundle autoload broken (HealthController canary)

Implementation gotcha (caught during dev): the host binary must be
COPIED into the staged layout, not symlinked. Qt's
applicationDirPath() reads /proc/self/exe which dereferences
symlinks, so a symlinked host would resolve to the original build/
dir and the supervisor would hunt for frankenphp + symfony there
instead of the staged tree. Real AppImages copy the binary, mimicking
that here.

Wiring:

  - examples/todo/Makefile: extracted the staging-symfony logic out
    of the appimage target into its own staging-symfony target. New
    integration-bundled target depends on `build` + `staging-symfony`
    and runs tests/bundled-supervisor.sh. quality target now invokes
    integration-bundled after the existing dev-mode integration test.
  - .gitea/workflows/ci.yml: new "Bundled-mode supervisor integration
    test" step right after the dev-mode integration step.

Closes the v0.1.1 follow-up "Bundled-mode integration test" tracked
in PLAN.md §13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:36:21 +02:00