Repository navigation
fix(cli): a Windows browser close ends the whole Chrome tree while its root is alive - #6789
miguel-heygen wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 1 blocker · 1 important · 0 nits · head f4ac1f16 (the blocker is unchanged at a9de586c)
fails without the fix: line verified locally: restoring child.kill("SIGKILL") fails the new test. ↓ inline
Scope
Windows render verification run 41001527004 (engine-cli) at f4ac1f16: launch.boundedClose.test.ts > kills a browser that acknowledged close but never exits… fails with Cannot read properties of undefined (reading 'catch'). I reproduced it on Linux by setting the platform to win32 in that test. a9de586c adds only launch.windowsTree.browser.test.ts, so the same failure remains on the required Tests on windows-latest.
| import { afterEach, expect, it, vi } from "vitest"; | ||
| import { launchManagedBrowser } from "./launch.js"; | ||
|
|
||
| const treeKill = vi.hoisted(() => vi.fn<(pid: number) => Promise<void>>()); |
There was a problem hiding this comment.
blocker — On Windows CI (process.platform really is win32), the first test goes through the force path too. There treeKill is a bare vi.fn() that returns undefined, so terminateWindowsProcessTree(pid).catch throws a TypeError, close() rejects, and the required Tests on windows-latest engine-cli lane goes red (run 41001527004). Give the mock a default resolved implementation, and re-apply it after mockReset:
| const treeKill = vi.hoisted(() => vi.fn<(pid: number) => Promise<void>>()); | |
| const treeKill = vi.hoisted(() => vi.fn<(pid: number) => Promise<void>>(async () => {})); |
Then use treeKill.mockReset().mockImplementation(async () => {}) in afterEach, or mockClear().
|
|
||
| async function killBrowser(child: ChildProcess): Promise<void> { | ||
| if (process.platform === "win32" && child.pid !== undefined) { | ||
| await terminateWindowsProcessTree(child.pid).catch(() => undefined); |
There was a problem hiding this comment.
important — This await has no bound. If taskkill stalls (AV scan, a hung process under load), close() never returns, and that undoes the bounded-close guarantee closedWithoutKill exists for (grace has already fired, and exitLimit only starts after this). Race it against CLOSE_GRACE_MS so the root SIGKILL always follows.
|
Closing: the hypothesis (a forced close leaves Chrome children alive on Windows) was not shown on a real Windows runner. With only the real-Chrome test and no fix, the test passes (run 41002532567). A repro for the hard-killed-CLI case will come as a separate PR. |
Edit accuracy: accurate 271 (base branch 271), smooth 145 of thoseThe gate passes. Quarantined, measured but not gated (0) |
What
On Windows, when the CLI has to force a browser closed (check, snapshot, render), it now ends Chrome's whole process tree while the root process is still alive, instead of only killing the root.
Why
After a force close,
child.kill("SIGKILL")on Windows ends only the root. Chrome's renderer, GPU and utility processes are not ended with it and Windows does not reparent them, sotaskkill /Tcan no longer find them once the root is gone. They kept the browser executable open, and a later run that touched the install failed withEPERM ... chrome-headless-shell.exe. The only other cleanup is a detached PowerShell sweep that is not awaited.Related work
#6771 and #6776 cover deleting a leftover browser folder that Windows still holds; they do not stop the processes. This removes the cause.
How
closedWithoutKillcallsterminateWindowsProcessTree(pid)(existingtaskkill /T /F, output ignored) beforechild.kill, only on Windows and only when the graceful close did not finish. A failed or already-gonetaskkillis ignored, and the root kill still follows.Test plan
fails without the fix: with the old
child.kill("SIGKILL")restored,on Windows ends the browser's whole tree while its root still lives, since children outlive a killed rootfails withexpected "vi.fn()" to be called with arguments.What I measured
launch.boundedClose.test.tsandlaunch.test.ts: 4 passed on three runs on Linux; oxlint and oxfmt clean. The new test sets the platform to win32 and checks the tree kill runs before the root is killed.What I did NOT execute
A real Windows machine. The cause is reasoned from the code and the reported errors, not reproduced there. The snapshot message
process <pid> child of <pid> could not be terminatedwas not traced to a source in this repo. TheEPERMin the Desktop app's own copy of the browser is Desktop's code, not this repo's.