mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
fix: headed browser no longer auto-shuts down after 15 seconds
The parent-process watchdog in server.ts polls the spawning CLI's PID every 15s and self-terminates if it is gone. The connect command in cli.ts exits with process.exit(0) immediately after launching the server, so the watchdog would reliably kill the headed browser within ~15s. This contradicted the idle timer's own design: server.ts:745 explicitly skips headed mode because "the user is looking at the browser. Never auto-die." The watchdog had no such exemption. Two-layer fix: 1. CLI layer: connect handler always sets BROWSE_PARENT_PID=0 (was only pass-through for pair-agent subprocesses). The user owns the headed browser lifecycle; cleanup happens via browser disconnect event or $B disconnect. 2. CLI layer: startServer() honors caller's BROWSE_PARENT_PID=0 in the headless spawn path too. Lets CI, non-interactive shells, and Claude Code Bash calls opt into persistent servers across short-lived CLI invocations. 3. Server layer: defense-in-depth. Watchdog now also skips when BROWSE_HEADED=1, so even if a future launcher forgets PID=0, headed browsers won't die. Adds log lines when the watchdog is disabled so lifecycle debugging is easier. Four community contributors diagnosed variants of this bug independently. Thanks for the clear analyses and reproductions. Closes #1020 (rocke2020) Closes #1018 (sanghyuk-seo-nexcube) Closes #1012 (rodbland2021) Closes #986 (jbetala7) Closes #1006 Closes #943 Co-Authored-By: rocke2020 <noreply@github.com> Co-Authored-By: sanghyuk-seo-nexcube <noreply@github.com> Co-Authored-By: rodbland2021 <noreply@github.com> Co-Authored-By: jbetala7 <noreply@github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-7
@@ -210,12 +210,18 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
|
||||
let proc: any = null;
|
||||
|
||||
// Allow the caller to opt out of the parent-process watchdog by setting
|
||||
// BROWSE_PARENT_PID=0 in the environment. Useful for CI, non-interactive
|
||||
// shells, and short-lived Bash invocations that need the server to outlive
|
||||
// the spawning CLI. Defaults to the current process PID (watchdog active).
|
||||
const parentPid = process.env.BROWSE_PARENT_PID === '0' ? '0' : String(process.pid);
|
||||
|
||||
if (IS_WINDOWS && NODE_SERVER_SCRIPT) {
|
||||
// Windows: Bun.spawn() + proc.unref() doesn't truly detach on Windows —
|
||||
// when the CLI exits, the server dies with it. Use Node's child_process.spawn
|
||||
// with { detached: true } instead, which is the gold standard for Windows
|
||||
// process independence. Credit: PR #191 by @fqueiro.
|
||||
const extraEnvStr = JSON.stringify({ BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: String(process.pid), ...(extraEnv || {}) });
|
||||
const extraEnvStr = JSON.stringify({ BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: parentPid, ...(extraEnv || {}) });
|
||||
const launcherCode =
|
||||
`const{spawn}=require('child_process');` +
|
||||
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
|
||||
@@ -226,7 +232,7 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
// macOS/Linux: Bun.spawn + unref works correctly
|
||||
proc = Bun.spawn(['bun', 'run', SERVER_SCRIPT], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: String(process.pid), ...extraEnv },
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile, BROWSE_PARENT_PID: parentPid, ...extraEnv },
|
||||
});
|
||||
proc.unref();
|
||||
}
|
||||
@@ -826,12 +832,12 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
||||
BROWSE_HEADED: '1',
|
||||
BROWSE_PORT: '34567',
|
||||
BROWSE_SIDEBAR_CHAT: '1',
|
||||
// Disable parent-process watchdog: the user controls the headed browser
|
||||
// window lifecycle. The CLI exits immediately after connect, so watching
|
||||
// it would kill the server ~15s later. Cleanup happens via browser
|
||||
// disconnect event or $B disconnect.
|
||||
BROWSE_PARENT_PID: '0',
|
||||
};
|
||||
// If parent explicitly set BROWSE_PARENT_PID=0 (pair-agent disabling
|
||||
// self-termination), pass it through so startServer doesn't override it.
|
||||
if (process.env.BROWSE_PARENT_PID === '0') {
|
||||
serverEnv.BROWSE_PARENT_PID = '0';
|
||||
}
|
||||
const newState = await startServer(serverEnv);
|
||||
|
||||
// Print connected status
|
||||
|
||||
+13
-1
@@ -757,8 +757,16 @@ const idleCheckInterval = setInterval(() => {
|
||||
// server can become an orphan — keeping chrome-headless-shell alive and
|
||||
// causing console-window flicker on Windows. Poll the parent PID every 15s
|
||||
// and self-terminate if it is gone.
|
||||
//
|
||||
// Headed mode (BROWSE_HEADED=1 or BROWSE_PARENT_PID=0): The user controls
|
||||
// the browser window lifecycle. The CLI exits immediately after connect,
|
||||
// so the watchdog would kill the server prematurely. Disabled in both cases
|
||||
// as defense-in-depth — the CLI sets PID=0 for headed mode, and the server
|
||||
// also checks BROWSE_HEADED in case a future launcher forgets.
|
||||
// Cleanup happens via browser disconnect event or $B disconnect.
|
||||
const BROWSE_PARENT_PID = parseInt(process.env.BROWSE_PARENT_PID || '0', 10);
|
||||
if (BROWSE_PARENT_PID > 0) {
|
||||
const IS_HEADED_WATCHDOG = process.env.BROWSE_HEADED === '1';
|
||||
if (BROWSE_PARENT_PID > 0 && !IS_HEADED_WATCHDOG) {
|
||||
setInterval(() => {
|
||||
try {
|
||||
process.kill(BROWSE_PARENT_PID, 0); // signal 0 = existence check only, no signal sent
|
||||
@@ -767,6 +775,10 @@ if (BROWSE_PARENT_PID > 0) {
|
||||
shutdown();
|
||||
}
|
||||
}, 15_000);
|
||||
} else if (IS_HEADED_WATCHDOG) {
|
||||
console.log('[browse] Parent-process watchdog disabled (headed mode)');
|
||||
} else if (BROWSE_PARENT_PID === 0) {
|
||||
console.log('[browse] Parent-process watchdog disabled (BROWSE_PARENT_PID=0)');
|
||||
}
|
||||
|
||||
// ─── Command Sets (from commands.ts — single source of truth) ───
|
||||
|
||||
Reference in New Issue
Block a user