mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
fix(setup): remove duplicate server spawn; verify health post-install (#72)
## 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: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 ──────────────────────────────────
|
// ── Step 7: Install auto-start on boot ──────────────────────────────────
|
||||||
if (!DRY_RUN) {
|
if (!DRY_RUN) {
|
||||||
console.log("\n🔄 Installing auto-start on login...\n");
|
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");
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user