fix: audit fallout — tunnel port, orphaned dev processes, scrollback clear

Auditing for repeats of the `pnpm dev --tunnel` bugs turned up the same
mistakes elsewhere.

Remote tunnels assumed 4040. ProjectEngine's Cloudflare quick tunnel
hardcoded http://localhost:4040, so a dashboard on an explicit --port, a PORT
override, or runDashboard's EADDRINUSE rebind published a PUBLIC tunnel to
whatever else held 4040. The dashboard now records its bound port
(setLocalDashboardPort, from both runDashboard and headless serve) and the
tunnel reads it, keeping 4040 only as the pre-report default.
register-discovery-routes already derived its port from req.socket.localPort
and is untouched.

Stopping the dev wrapper orphaned everything it started. It installed no
signal handlers, so teardown only ran from the child's close handler:
signalling the wrapper left the dev server AND its cloudflared alive —
observed twice, four surviving processes each time, including a public
trycloudflare URL still serving a dev server believed to be down. Ctrl-C hid
it by signalling the whole process group.

SessionTerminal appended scrollback instead of clearing first, though the
server sends it as a separate frame precisely so the client can clear. Latent
today because every reattach builds a fresh xterm; a duplicated-history bug
the moment an in-place reconnect appears.

And BackupManager's centralDbPath is gone: written, never read, and a
leftover of the removed SQLite backup — the same class of stale artifact that
onboarding was using as evidence about a Postgres install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-08-18 21:09:03 -07:00
parent b67e3aa8bc
commit 16e63462cc
14 changed files with 257 additions and 13 deletions

View File

@@ -315,6 +315,37 @@ async function warnIfDistStale() {
}
}
/*
FNXC:DevWorkflow 2026-08-19-04:00:
Stop the dev server AND the tunnel when this supervisor is signalled. Teardown used to live only in
the child's `close` handler, so `kill <wrapper-pid>` (or any supervisor-style stop) killed the
wrapper and left the dev server and its cloudflared running as orphans — observed twice, four
processes surviving each time. Interactive Ctrl-C hid it because the terminal signals the whole
process group; anything that signals only this process did not.
An orphaned tunnel is the dangerous half: a public trycloudflare URL keeps serving the dev server
after the operator believes it is down. Forward the signal, give the child a moment to exit on its
own, then leave.
*/
let shuttingDown = false;
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
process.on(signal, () => {
if (shuttingDown) return;
shuttingDown = true;
devTunnel?.stop?.();
if (appChild && !appChild.killed) {
appChild.kill(signal === "SIGHUP" ? "SIGTERM" : signal);
// The child owns a graceful shutdown path (draining agents, stopping Postgres); give it room,
// then stop waiting so a wedged child cannot pin the terminal open.
const forceExit = setTimeout(() => process.exit(0), 10_000);
forceExit.unref?.();
appChild.once("close", () => process.exit(0));
return;
}
process.exit(0);
});
}
await warnIfDistStale();
if (!prebuildCommand) {