refactor: migrate Bun.spawn to Process utility with timeout and cleanup (#14448)

This commit is contained in:
Dax
2026-02-24 23:04:15 -05:00
committed by GitHub
parent da40ab7b3d
commit 814c1d398c
13 changed files with 203 additions and 114 deletions

View File

@@ -0,0 +1,71 @@
import { spawn as launch, type ChildProcess } from "child_process"
export namespace Process {
export type Stdio = "inherit" | "pipe" | "ignore"
export interface Options {
cwd?: string
env?: NodeJS.ProcessEnv | null
stdin?: Stdio
stdout?: Stdio
stderr?: Stdio
abort?: AbortSignal
kill?: NodeJS.Signals | number
timeout?: number
}
export type Child = ChildProcess & { exited: Promise<number> }
export function spawn(cmd: string[], options: Options = {}): Child {
if (cmd.length === 0) throw new Error("Command is required")
options.abort?.throwIfAborted()
const proc = launch(cmd[0], cmd.slice(1), {
cwd: options.cwd,
env: options.env === null ? {} : options.env ? { ...process.env, ...options.env } : undefined,
stdio: [options.stdin ?? "ignore", options.stdout ?? "ignore", options.stderr ?? "ignore"],
})
let aborted = false
let timer: ReturnType<typeof setTimeout> | undefined
const abort = () => {
if (aborted) return
if (proc.exitCode !== null || proc.signalCode !== null) return
aborted = true
proc.kill(options.kill ?? "SIGTERM")
const timeout = options.timeout ?? 5_000
if (timeout <= 0) return
timer = setTimeout(() => {
proc.kill("SIGKILL")
}, timeout)
}
const exited = new Promise<number>((resolve, reject) => {
const done = () => {
options.abort?.removeEventListener("abort", abort)
if (timer) clearTimeout(timer)
}
proc.once("exit", (exitCode, signal) => {
done()
resolve(exitCode ?? (signal ? 1 : 0))
})
proc.once("error", (error) => {
done()
reject(error)
})
})
if (options.abort) {
options.abort.addEventListener("abort", abort, { once: true })
if (options.abort.aborted) abort()
}
const child = proc as Child
child.exited = exited
return child
}
}