fix(browse): Chromium-crash exit is daemon-only — embedded launches never kill their host

handleChromiumDisconnect unconditionally process.exit()ed. Correct for
the standalone daemon (its supervisor/user must notice); suicidal when
a TEST launches BrowserManager in-process: a mid-suite Chromium death
exited the whole bun shard with no terminal summary — the exact
truncation class the strict runner flags (observed live: CI shard 1 on
eb233299 died at cache-concurrent-refresh right after a daemon-spawning
gate test; with this fix the same pairing runs to completion and
REPORTS instead of dying).

The standalone entrypoint opts in via markDaemonProcess() under
server.ts's import.meta.main gate — the same embedder contract its
signal handlers already use (gbrowser phoenix keeps its own handlers).
Embedded contexts now get the disconnect log line and continue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Test
2026-08-29 09:33:47 +00:00
co-authored by Claude Fable 5
parent eb2332995a
commit 36c41055e2
2 changed files with 27 additions and 3 deletions
+22 -2
View File
@@ -186,12 +186,32 @@ export async function resolveDisconnectCause(browser: Browser | null): Promise<'
}
/**
* Headless `launch()` disconnect handler. Exits 0 on clean user-quit, 1 on
* crash. Inlined into the launch() body via a one-line dispatch so
* Exit-on-disconnect is DAEMON-ONLY semantics. The standalone server
* entrypoint opts in via markDaemonProcess() (under its import.meta.main
* gate, same contract as its signal handlers); embedders — gbrowser
* phoenix, and every test that launches a BrowserManager in-process —
* must never have a Chromium crash process.exit() their HOST. Observed
* live before this flag: a test-launched browser died mid-suite and the
* exit(1) killed the whole bun shard with no terminal summary (the
* truncation class the strict runner exists to catch).
*/
let daemonProcess = false;
export function markDaemonProcess(): void {
daemonProcess = true;
}
/**
* Headless `launch()` disconnect handler. In the standalone daemon: exits 0
* on clean user-quit, 1 on crash. Embedded contexts get the log line only.
* Inlined into the launch() body via a one-line dispatch so
* browser-manager's flow stays grep-friendly.
*/
export async function handleChromiumDisconnect(browser: Browser | null): Promise<void> {
const cause = await resolveDisconnectCause(browser);
if (!daemonProcess) {
console.error(`[browse] Chromium disconnected (${cause}) in an embedded context — host process continues.`);
return;
}
if (cause === 'clean') {
console.error('[browse] Chromium closed cleanly (user-initiated quit). Server exiting (0).');
process.exit(0);
+5 -1
View File
@@ -13,7 +13,7 @@
* Port: random 10000-60000 (or BROWSE_PORT env for debug override)
*/
import { BrowserManager } from './browser-manager';
import { BrowserManager, markDaemonProcess } from './browser-manager';
import { handleReadCommand, hasOutArg } from './read-commands';
import { handleWriteCommand } from './write-commands';
import { handleMetaCommand } from './meta-commands';
@@ -1394,6 +1394,10 @@ async function handleCommand(body: any, tokenInfo?: TokenInfo | null): Promise<R
// server.ts as a submodule can register their own signal handlers without
// fighting with gstack's. CLI path is unchanged.
if (import.meta.main) {
// Standalone daemon: a Chromium crash must exit THIS process (its
// supervisor/user notices); embedders and in-process test launches must
// never be exited by browser-manager's disconnect handler.
markDaemonProcess();
// SIGINT (Ctrl+C): user intentionally stopping → shutdown.
process.on('SIGINT', () => activeShutdown?.());
// SIGHUP (terminal hangup): with handleSIGHUP:false at the three launch