Files
orca/src/main/codex/codex-app-server-process-tree-kill.ts
T
Brennan Benson 7f8ca49e3d fix(codex): start short-lived Codex app-servers on Windows without cmd.exe (#23830)
* fix(codex): start short-lived Codex app-servers on Windows without cmd.exe

Orca's short-lived Codex app-server launches (history index passes, the
state-DB recovery claimant, hook trust grants, and the rate-limit and model
catalog probes) wrapped npm's codex.cmd in `cmd.exe /d /c` before handing it
to spawnProcess. That hid the shim from spawnProcess's resolver, so every one
of these launches went through cmd.exe instead of straight to node.

Hand the bare CLI path to the spawn chokepoint like the chat session already
does. The rate-limit probe and the recovery claimant also move off direct
node:child_process imports, shrinking that allowlist by two.

* chore(codex): drop stale cmd.exe premises from the probe kill and CLI pairing comments

Short-lived Codex app-servers now reach spawnProcess as the bare shim, so the
direct child is node for a resolved npm shim and cmd.exe only as the fallback.

* test(child-process): delist the rate-limit probe from the hidden-console ratchet

The probe now spawns through spawnProcess, which always hides the console.
2026-09-29 17:52:22 -07:00

77 lines
2.7 KiB
TypeScript

import { spawnProcess } from '../../shared/child-process/run-process'
import type { ChildProcessHandle, ProcessSpec } from '../../shared/child-process/process-spec'
import { admitProcessTreeKill } from '../../shared/child-process/process-tree-kill-gate'
/** Spawn seam for tests; production always goes through the hardened spawnProcess wrapper. */
export type CodexAppServerSpawn = (
program: string,
args: string[],
options: Record<string, unknown>
) => ChildProcessHandle
export const spawnCodexAppServerProcess: CodexAppServerSpawn = (program, args, options) =>
spawnProcess({ program, args, ...options } as ProcessSpec)
export function killCodexAppServerProcessTree(
child: Pick<ChildProcessHandle, 'pid' | 'kill'>,
options: { platform?: NodeJS.Platform; spawnImpl?: CodexAppServerSpawn } = {}
): void {
const platform = options.platform ?? process.platform
const spawnImpl = options.spawnImpl ?? spawnCodexAppServerProcess
if (platform === 'win32' && child.pid) {
if (
!admitProcessTreeKill({
pid: child.pid,
site: 'codex-app-server-session-deadline',
scope: 'win-taskkill-tree'
})
) {
// Refusal blocks the tree walk, not the termination: the root kill is
// handle-addressed, so it cannot reach the recycled pid we refused.
child.kill('SIGKILL')
return
}
try {
// Why: npm-installed Codex runs behind a launcher (node, or cmd.exe for an unresolved shim);
// killing only that wrapper leaves the app-server child alive after a timeout.
const killer = spawnImpl('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
stdio: 'ignore',
windowsHide: true
})
let fellBack = false
const killDirectChild = (): void => {
if (!fellBack) {
fellBack = true
child.kill('SIGKILL')
}
}
killer.on('error', killDirectChild)
killer.on('exit', (code) => {
if (code !== 0) {
killDirectChild()
}
})
killer.unref()
return
} catch {
// Fall through to the direct-child best effort when taskkill cannot start.
}
}
if (child.pid) {
try {
// npm/package-manager launchers insert a shim child on POSIX. Reap its
// direct descendants before signalling the wrapper itself.
const descendants = spawnImpl('pkill', ['-KILL', '-P', String(child.pid)], {
stdio: 'ignore'
})
// A missing pkill surfaces as an async 'error' event, and an unhandled one
// takes down the main process.
descendants.on('error', () => undefined)
descendants.unref()
} catch {
// The direct kill below remains the fallback when pkill is unavailable.
}
}
child.kill('SIGKILL')
}