chrome guard: a zombie counts as ended (an orphan's new parent in a container whose PID 1 is no init never reaps it, and kill(pid, 0) still succeeds), so the reaper reports it and the real-process test passes on the Linux runner

This commit is contained in:
CRTkafa 2026-10-15 00:49:56 +08:00
parent c3b1e8c370
commit cdaecc2afb
2 changed files with 11 additions and 3 deletions

View file

@ -63,8 +63,16 @@ export function staleChromes(rows: PsRow[], markers: string[] = PROFILE_MARKERS)
return { reap, windows };
}
/** A zombie has ended: it only waits for its parent to reap it. An orphan's new parent may never do that (a container
* whose PID 1 is not an init), and kill(pid, 0) still succeeds on it. */
export function isZombie(pid: number): boolean {
if (process.platform === 'win32') return false;
try { return /^Z/.test(rt.spawnSync(['ps', '-o', 'stat=', '-p', String(pid)], { stdout: 'pipe', stderr: 'ignore', timeout: 5000 }).stdout.toString().trim()); } catch { return false; }
}
function alive(pid: number): boolean {
try { process.kill(pid, 0); return true; } catch (e) { return (e as NodeJS.ErrnoException).code === 'EPERM'; }
try { process.kill(pid, 0); } catch (e) { return (e as NodeJS.ErrnoException).code === 'EPERM'; }
return !isZombie(pid);
}
/** SIGTERM (Chromium's clean shutdown), then SIGKILL after `graceMs`. True when the process is gone. */

View file

@ -4,7 +4,7 @@
// standing in for Chrome (sh + sleep, no browser, about a second): the watchdog ends the child when its parent dies and
// never touches a reused pid; the reaper ends a real orphan with a storebound profile on its command line.
import { describe, expect, test } from 'bun:test';
import { guardChrome, guardedPids, isOrphan, parsePs, reapStaleChromes, staleChromes, startWatchdog, userDataDirOf, watchdogArgv } from '../cli/console/chrome-guard';
import { guardChrome, guardedPids, isOrphan, isZombie, parsePs, reapStaleChromes, staleChromes, startWatchdog, userDataDirOf, watchdogArgv } from '../cli/console/chrome-guard';
import { persistentExecutable } from '../cli/shots/browser';
import * as rt from '../cli/lib/rt';
@ -91,7 +91,7 @@ describe('headless runs on macOS never use Google Chrome.app when another Chromi
// ---- real processes (sh + sleep stand in for Chrome and its run) ----
const posix = process.platform !== 'win32';
const alive = (pid: number) => { try { process.kill(pid, 0); return true; } catch { return false; } };
const alive = (pid: number) => { try { process.kill(pid, 0); } catch { return false; } return !isZombie(pid); };
async function until(what: string, ok: () => boolean, ms = 5000): Promise<void> {
const end = Date.now() + ms;
while (!ok()) { if (Date.now() > end) throw new Error(`timed out after ${ms} ms waiting for: ${what}`); await rt.sleep(50); }