From cdaecc2afb157f777349ed5af3d558c3adc60aa0 Mon Sep 17 00:00:00 2001 From: CRT Date: Thu, 15 Oct 2026 00:49:56 +0800 Subject: [PATCH] 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 --- cli/console/chrome-guard.ts | 10 +++++++++- tests/chrome-guard.test.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cli/console/chrome-guard.ts b/cli/console/chrome-guard.ts index 48583e3..aa4c943 100644 --- a/cli/console/chrome-guard.ts +++ b/cli/console/chrome-guard.ts @@ -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. */ diff --git a/tests/chrome-guard.test.ts b/tests/chrome-guard.test.ts index 16d32a3..39fa199 100644 --- a/tests/chrome-guard.test.ts +++ b/tests/chrome-guard.test.ts @@ -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 { 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); }