v0.2.0 (12/N): bundled-mode port negotiation
PLAN.md §13 v0.2.0 *Bundled-mode port negotiation*. Hardcoded
m_port = 8765 used to fail loudly only when a second php-qml app
launched on the same machine — whichever lost the bind race went
Offline with no recovery path.
Fix:
- Bind a transient QTcpServer to QHostAddress::LocalHost port 0,
read serverPort(), close. Linux's ephemeral-port allocator
doesn't immediately reassign the closed port, and FrankenPHP's
bind happens within milliseconds inside spawnChild() — small
TOCTOU window in theory, fail-loud in practice if it ever races.
- BRIDGE_PORT env override pins the port for tests / dev
(bundled-supervisor.sh and perfsmoke.sh now both export it
instead of the previous PERF_BACKEND_PORT-only knob).
- writePortSentinel() drops the chosen port to
$XDG_DATA_HOME/<app>/var/bridge.port so external tools can read
the runtime address without parsing Qt's log output.
Caddyfile already supported {$PORT:8765} env interpolation, so
no template churn. MERCURE_URL is computed from m_url which is
re-derived from the chosen port — no .env changes needed for
bundled mode (dev mode .env still references :8765 since the
developer controls their own frankenphp invocation).
bundled-supervisor.sh integration test gained a sentinel-file
assertion: after first launch, $USER_DATA/var/bridge.port must
exist and contain BRIDGE_PORT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,10 @@ STAGING="$APP_DIR/build/staging-symfony"
|
||||
CADDYFILE="$APP_DIR/Caddyfile"
|
||||
|
||||
APP_NAME=todo
|
||||
# Force a known port so the test can pre-check / curl without
|
||||
# parsing the sentinel file. The supervisor honours BRIDGE_PORT
|
||||
# in bundled mode (PLAN.md §13 v0.2.0 *Port negotiation*); in
|
||||
# real-world use it negotiates a free ephemeral port instead.
|
||||
PORT=8765
|
||||
|
||||
step() { echo "→ $*"; }
|
||||
@@ -52,6 +56,8 @@ if (echo > "/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then
|
||||
skip "port $PORT already in use (dev instance running?)"
|
||||
fi
|
||||
|
||||
export BRIDGE_PORT="$PORT"
|
||||
|
||||
# ── Stage a fake AppImage layout in a temp dir ─────────────────────────
|
||||
ROOT="$(mktemp -d)"
|
||||
DATA_DIR="$(mktemp -d)"
|
||||
@@ -195,6 +201,13 @@ echo "$HEALTHZ2_BODY" | grep -q '"status":"ok"' \
|
||||
# ── Pre-migration auto-backup ─────────────────────────────────────────
|
||||
# Launch 1 created data.sqlite; launch 2 should have copied it to
|
||||
# data.sqlite.<unix-timestamp>.bak before re-running migrations.
|
||||
# ── Port-negotiation sentinel ─────────────────────────────────────────
|
||||
step "verify port sentinel was written"
|
||||
SENTINEL="$USER_DATA/var/bridge.port"
|
||||
[ -f "$SENTINEL" ] || fail "expected $SENTINEL after first launch"
|
||||
SENTINEL_PORT="$(cat "$SENTINEL" | tr -d '[:space:]')"
|
||||
[ "$SENTINEL_PORT" = "$PORT" ] || fail "sentinel port mismatch: got '$SENTINEL_PORT', expected '$PORT'"
|
||||
|
||||
step "verify pre-migration backup of data.sqlite was written"
|
||||
shopt -s nullglob
|
||||
backups=( "$USER_DATA"/var/data.sqlite.*.bak )
|
||||
|
||||
@@ -61,6 +61,10 @@ export XDG_DATA_HOME="$DATA_DIR/share"
|
||||
export XDG_CACHE_HOME="$DATA_DIR/cache"
|
||||
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||
mkdir -p "$XDG_DATA_HOME" "$XDG_CACHE_HOME"
|
||||
# Force the port so the curl probe below can hit a known address —
|
||||
# the supervisor would otherwise negotiate a free ephemeral port and
|
||||
# we'd have to read it back from the sentinel file.
|
||||
export BRIDGE_PORT="$PERF_BACKEND_PORT"
|
||||
|
||||
step "launching AppImage (${RUNNER[*]:-direct})"
|
||||
START_NS=$(date +%s%N)
|
||||
|
||||
Reference in New Issue
Block a user