mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
`gh` and `glab` on PATH are routinely shims — mise, asdf, volta, or a hand-written wrapper — so a timed-out invocation has a chain to stop, not one process. `execFileCapture`'s POSIX kill path signals only the direct child; the descendants are orphaned to init and keep running. #18234 is exactly that shape: `bash ~/.local/bin/gh` -> `mise x gh` -> `gh`, where the reporter found the tail reparented to `systemd --user` and still at 100% CPU nearly two hours later. The 15s deadline #18239 added bounds Orca's semaphore slot and its promise; it does not bound the CPU burn. Route both CLIs through `execFileCaptureToTermination`, the primitive git's barrier path already uses: POSIX children spawn `detached`, the deadline signals `-pgid` and escalates to SIGKILL, and the promise waits for verified termination. Windows behaviour is unchanged (`taskkill /t` either way). Switching primitives also swapped execFile's hard maxBuffer failure for `runProcess`'s silent clipping, which would have turned an oversized gh response into a shorter valid-looking one. `ProcessResult` now reports truncation and the capture rejects on it, restoring the old contract and closing the same latent gap on git's barrier path.
74 lines
3.0 KiB
TypeScript
74 lines
3.0 KiB
TypeScript
// The public contract for Orca's single child-process entry point. Split from
|
|
// run-process.ts so the runner stays under its line cap; import the runtime
|
|
// functions from run-process, which re-exports everything here.
|
|
import type { ChildProcess, SpawnOptions as NodeSpawnOptions } from 'node:child_process'
|
|
|
|
export type ChildProcessHandle = ChildProcess
|
|
|
|
export type SpawnedProcess = ChildProcess
|
|
|
|
/**
|
|
* The single place Orca starts a child process.
|
|
*
|
|
* Why one place: six decisions have to be made every time a child is spawned,
|
|
* POSIX forgives all six, and Windows punishes each of them differently —
|
|
* console visibility, argument quoting, `.cmd` interpretation, binary
|
|
* resolution, timeout policy, and how the tree is later terminated. Made
|
|
* per-call-site, they were right in some files and wrong in others, and the
|
|
* wrong ones reached users as stolen keyboard focus, mangled agent prompts and
|
|
* orphaned process trees.
|
|
*
|
|
* Callers outside this directory must not import `node:child_process`; a guard
|
|
* test enforces that against a shrinking allowlist.
|
|
*/
|
|
|
|
export type ProcessSpec = {
|
|
/**
|
|
* Program to run. On Windows this should already be an absolute path —
|
|
* spawning by bare name depends on the child's PATH, which under Group Policy
|
|
* or a stripped Electron environment can resolve to nothing.
|
|
*/
|
|
program: string
|
|
args?: readonly string[]
|
|
cwd?: string
|
|
env?: NodeJS.ProcessEnv
|
|
/** Kill the process (and, on Windows, its console) after this long. */
|
|
timeoutMs?: number | null
|
|
/** Written to stdin then closed. Omit to leave stdin empty and closed. */
|
|
input?: string
|
|
/** Cap on captured stdout/stderr; output past it is discarded. */
|
|
maxOutputBytes?: number
|
|
/** Kills the process when aborted; the result still reports the exit. */
|
|
signal?: AbortSignal
|
|
/** Keep the child in its own POSIX process group for tree termination. */
|
|
detached?: boolean
|
|
/** Preserve a caller-owned Windows command line such as a cmd.exe invocation. */
|
|
windowsVerbatimArguments?: boolean
|
|
/** Streaming callers may suppress child output for auxiliary processes. */
|
|
stdio?: NodeSpawnOptions['stdio']
|
|
/** Kill the whole process tree and do not settle until termination is verified. */
|
|
terminationBarrier?: boolean | ProcessTerminationBarrier
|
|
/** Called once when the child exits or tree termination is verified. */
|
|
onChildTerminated?: () => void
|
|
}
|
|
|
|
export type ProcessTerminationBarrier = {
|
|
observeStderr?: (chunk: Buffer | string) => void
|
|
signal: (child: ChildProcess, signal?: NodeJS.Signals) => Promise<boolean>
|
|
force: (child: ChildProcess) => Promise<boolean>
|
|
}
|
|
|
|
export type ProcessResult = {
|
|
code: number | null
|
|
signal: NodeJS.Signals | null
|
|
stdout: string
|
|
stderr: string
|
|
/** True when the process was killed by `timeoutMs` rather than exiting. */
|
|
timedOut: boolean
|
|
/** True when stdout or stderr exceeded `maxOutputBytes` and was clipped. */
|
|
outputTruncated?: boolean
|
|
}
|
|
|
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30_000
|
|
export const DEFAULT_MAX_OUTPUT_BYTES = 8 * 1024 * 1024
|