Files
orca/src/main/git/command-runner/spawned-command-tree-kill.ts
T
Neil 9db1b4ce6e fix(crash-reporting): make the own-Chromium gate a real choke point
Round-3 review found the guard was not the choke point its own comments
claimed: six pid-addressed `taskkill /pid <pid> /t /f` families in main were
ungated and uninstrumented, so the stale-pid shape stayed producible and a
`selfInitiatedTreeKillCount: 0` could read as exculpatory when it was not.

- Gate the remaining main-process families: the git command-runner abort, the
  notebook-cell and automation-precheck timeouts.
- Turn the `src/shared` seam into the gate itself (`process-tree-kill-gate`), so
  the runProcess choke point, the codex app-server deadline kill and the
  ephemeral-VM recipe kill ask the same decision. Those three are compiled into
  the CLI/relay too and cannot import main; main installs the guard at preflight.
- Ratchet (`main-process-tree-kill-gate.test.ts`): a new pid-addressed taskkill
  in main that skips the gate fails, and the allowlist entries must still exist.
- Give pid-addressed kills eviction priority in the 32-entry ring: 32 routine
  `win-pty-job` teardowns from a window-close burst no longer evict the one
  entry that discriminates a self-kill from an external one.
- Correct the coverage doc, which described the uninstrumented Windows sites as
  POSIX `process.kill(-pid)` group kills and omitted the git and codex paths.
2026-09-03 04:07:42 -07:00

60 lines
1.6 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process'
import { admitSelfInitiatedTreeKill } from '../../own-chromium-tree-kill-guard'
const WINDOWS_TREE_KILL_WAIT_MS = 2_000
export function killSpawnedCommandTree(child: ChildProcess): Promise<void> {
const pid = child.pid
if (!pid || process.platform !== 'win32') {
child.kill()
return Promise.resolve()
}
if (
!admitSelfInitiatedTreeKill({ pid, site: 'git-command-tree-kill', scope: 'win-taskkill-tree' })
) {
return Promise.resolve()
}
return new Promise((resolve) => {
let killer: ChildProcess
try {
// Why: Windows shims/wsl.exe own descendants; wait for /t tree cleanup so a timed-out command can't outlive its probe.
killer = spawn('taskkill', ['/pid', String(pid), '/t', '/f'], {
stdio: 'ignore',
windowsHide: true
})
if (!killer || typeof killer.unref !== 'function') {
child.kill()
resolve()
return
}
} catch {
child.kill()
resolve()
return
}
let settled = false
let timer: NodeJS.Timeout | null = null
const finish = (fallbackToChildKill: boolean): void => {
if (settled) {
return
}
settled = true
if (timer) {
clearTimeout(timer)
}
killer.removeAllListeners()
if (fallbackToChildKill) {
child.kill()
}
resolve()
}
killer.once('error', () => finish(true))
killer.once('close', (code) => finish(code !== 0))
timer = setTimeout(() => {
killer.kill()
finish(true)
}, WINDOWS_TREE_KILL_WAIT_MS)
killer.unref()
})
}