// Low-level child-process helpers shared by the sandbox backends. // (Previously also held docker/podman detection; that's gone with the container // model — see sandbox.ts for bwrap / sandbox-exec backend detection.) import { spawn } from "node:child_process"; export interface CaptureResult { stdout: Buffer; stderr: string; code: number | null; } export interface SpawnOptions { input?: Buffer | string; signal?: AbortSignal; timeout?: number; // seconds cwd?: string; env?: NodeJS.ProcessEnv; } /** Low-level spawn that captures stdout as a Buffer (binary-safe). */ export function spawnCapture(bin: string, args: string[], opts: SpawnOptions = {}): Promise { return new Promise((resolve, reject) => { const child = spawn(bin, args, { stdio: ["pipe", "pipe", "pipe"], cwd: opts.cwd, env: opts.env }); const out: Buffer[] = []; let err = ""; let timedOut = false; let timer: NodeJS.Timeout | undefined; const onAbort = () => child.kill("SIGKILL"); opts.signal?.addEventListener("abort", onAbort, { once: true }); if (opts.timeout && opts.timeout > 0) { timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, opts.timeout * 1000); } child.stdout.on("data", (d: Buffer) => out.push(d)); child.stderr.on("data", (d: Buffer) => { err += d.toString("utf8"); }); child.on("error", (e) => { if (timer) clearTimeout(timer); opts.signal?.removeEventListener("abort", onAbort); reject(e); }); child.on("close", (code) => { if (timer) clearTimeout(timer); opts.signal?.removeEventListener("abort", onAbort); if (opts.signal?.aborted) return reject(new Error("aborted")); if (timedOut) return reject(new Error(`timeout:${opts.timeout}`)); resolve({ stdout: Buffer.concat(out), stderr: err, code }); }); if (opts.input !== undefined) child.stdin.write(opts.input); child.stdin.end(); }); } export interface StreamOptions { signal?: AbortSignal; timeout?: number; // seconds onData?: (chunk: Buffer) => void; // merged stdout+stderr cwd?: string; env?: NodeJS.ProcessEnv; } /** Streaming spawn: merges stdout+stderr to onData. Returns exit code. */ export function spawnStream(bin: string, args: string[], opts: StreamOptions = {}): Promise<{ exitCode: number | null }> { return new Promise((resolve, reject) => { const child = spawn(bin, args, { stdio: ["pipe", "pipe", "pipe"], cwd: opts.cwd, env: opts.env }); let timedOut = false; let timer: NodeJS.Timeout | undefined; const onAbort = () => child.kill("SIGKILL"); opts.signal?.addEventListener("abort", onAbort, { once: true }); if (opts.timeout && opts.timeout > 0) { timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, opts.timeout * 1000); } if (opts.onData) { child.stdout.on("data", opts.onData); child.stderr.on("data", opts.onData); } child.on("error", (e) => { if (timer) clearTimeout(timer); opts.signal?.removeEventListener("abort", onAbort); reject(e); }); child.on("close", (code) => { if (timer) clearTimeout(timer); opts.signal?.removeEventListener("abort", onAbort); if (opts.signal?.aborted) return reject(new Error("aborted")); if (timedOut) return reject(new Error(`timeout:${opts.timeout}`)); resolve({ exitCode: code }); }); child.stdin.end(); }); }