From eb6cd09bed52ff8d50e1c5c43f71e7b8b8b1d5b0 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Fri, 8 May 2026 14:52:27 +1000 Subject: [PATCH] fix(setup): remove duplicate server spawn; verify health post-install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Dogfood evidence (Pi231, 2026-05-08) A user ran `node setup.mjs --bind 0.0.0.0 --auth-mode multi` and got: - setup.mjs exit 0 - /health responded with authMode:"none" and server bound to 127.0.0.1 only - ps showed two server.mjs processes: one orphan from setup (wrong config), one systemd child restart-looping on EADDRINUSE ## Root cause (two-step conflict) Step 6 (the deleted block) called `execSync('bash "${startPath}"')` which ran start.sh's `nohup node server.mjs &` — spawning the server WITHOUT exporting CLAUDE_BIND or CLAUDE_AUTH_MODE. That server ran with default bind=127.0.0.1 and authMode=none, ignoring the user's CLI flags. Step 7 then wrote the systemd unit/launchd plist WITH the correct env vars and bootstrapped the service — but port 3456 was already taken by Step 6's spawn, causing EADDRINUSE and a silent restart loop. setup.mjs exited 0 because Step 6 had "succeeded" (a server was running, just the wrong one). ## What changed 1. Deleted Step 6 entirely (the `execSync('bash "${startPath}"')` block). The systemd/launchd service installed in Step 7 is now the sole authoritative start path. start.sh is unchanged and still available for manual non-systemd use. 2. Added Step 8 inside the `if (!DRY_RUN)` block: after Step 7 bootstrap, waits 3 s, then GETs http://127.0.0.1:${PORT}/health with a 5 s timeout. - On 200 OK: logs version, authMode, and bind socket (best-effort). - On failure: prints clear error pointing to service logs, exits 1. - Skipped when --no-start is set (existing flag). Identified during PR #71 dogfood testing on Pi231 (RPi4 / Debian Bookworm). Co-Authored-By: Claude Sonnet 4.6 --- setup.mjs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/setup.mjs b/setup.mjs index d3e0a19..e2754dd 100755 --- a/setup.mjs +++ b/setup.mjs @@ -264,13 +264,6 @@ console.log(` ╚══════════════════════════════════════════════════════════════╝ `); -// ── Step 6: Optionally start ──────────────────────────────────────────── -if (!SKIP_START && !DRY_RUN) { - try { - execSync(`bash "${startPath}"`, { stdio: "inherit" }); - } catch { /* ignore */ } -} - // ── Step 7: Install auto-start on boot ────────────────────────────────── if (!DRY_RUN) { console.log("\n🔄 Installing auto-start on login...\n"); @@ -405,4 +398,52 @@ WantedBy=default.target } console.log("\n✅ Auto-start installed — proxy will start automatically on login\n"); + + // ── Step 8: Post-install health verification ─────────────────────────── + if (!SKIP_START) { + console.log("⏳ Waiting for server to bind...\n"); + await new Promise(r => setTimeout(r, 3000)); + + const healthUrl = `http://127.0.0.1:${PORT}/health`; + let verified = false; + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 5000); + const res = await fetch(healthUrl, { signal: controller.signal }); + clearTimeout(timer); + + if (res.ok) { + const body = await res.json().catch(() => ({})); + console.log(` ✓ Health check passed (${healthUrl})`); + console.log(` version: ${body.version ?? "unknown"}`); + console.log(` authMode: ${body.authMode ?? "unknown"}`); + + // Verify bind socket + try { + const bindCheck = process.platform === "linux" + ? execSync(`ss -tlnp 2>/dev/null | grep ':${PORT}'`, { encoding: "utf-8" }).trim() + : execSync(`lsof -nP -iTCP:${PORT} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf-8" }).trim(); + if (bindCheck) { + console.log(` bind: ${bindCheck.split("\n")[0]}`); + } + } catch { /* bind check is best-effort */ } + + verified = true; + } else { + warn(`Health check returned HTTP ${res.status} — service may not have started cleanly`); + } + } catch (e) { + const isTimeout = e.name === "AbortError" || (e.cause && e.cause.code === "UND_ERR_CONNECT_TIMEOUT"); + warn(`Health check failed: ${isTimeout ? "timeout (5s)" : e.message}`); + } + + if (!verified) { + const logHint = process.platform === "linux" + ? "journalctl --user -u ocp-proxy -n 50" + : `tail -n 100 ~/.ocp/logs/proxy.log`; + console.error(`\n ✗ Server did not respond on port ${PORT} within 5 seconds.`); + console.error(` Check service logs:\n ${logHint}\n`); + process.exit(1); + } + } }