Repository Public
1/**2 * Subprocess helper for runtime-fs: argv arrays only, never a shell, bounded output, timeout.3 * Nothing here logs argv or output (they may contain secrets or file contents).4 */5import { spawn } from 'node:child_process';67export interface RunOptions {8 cwd?: string;9 env?: NodeJS.ProcessEnv;10 timeoutMs: number;11 /** Max stdout bytes kept; when exceeded the child is killed and `truncated` is set. */12 maxStdout?: number;13 maxStderr?: number;14}15export interface RunResult {16 code: number | null;17 stdout: string;18 stderr: string;19 truncated: boolean;20 timedOut: boolean;21 /** Spawn failure (e.g. ENOENT), otherwise null. */22 spawnError: string | null;23}2425export function run(file: string, argv: readonly string[], opts: RunOptions): Promise<RunResult> {26 const maxOut = opts.maxStdout ?? 1024 * 1024;27 const maxErr = opts.maxStderr ?? 64 * 1024;28 return new Promise((resolve) => {29 const child = spawn(file, [...argv], {30 cwd: opts.cwd,31 env: opts.env,32 shell: false,33 stdio: ['ignore', 'pipe', 'pipe'],34 });35 const out: Buffer[] = [];36 const err: Buffer[] = [];37 let outLen = 0;38 let errLen = 0;39 let truncated = false;40 let timedOut = false;41 let spawnError: string | null = null;42 let done = false;43 const timer = setTimeout(() => {44 timedOut = true;45 child.kill('SIGKILL');46 }, opts.timeoutMs);47 child.stdout.on('data', (b: Buffer) => {48 const room = maxOut - outLen;49 if (b.length > room) {50 if (room > 0) out.push(b.subarray(0, room));51 outLen = maxOut;52 truncated = true;53 child.kill('SIGKILL');54 } else {55 out.push(b);56 outLen += b.length;57 }58 });59 child.stderr.on('data', (b: Buffer) => {60 if (errLen >= maxErr) return;61 const part = b.subarray(0, maxErr - errLen);62 err.push(part);63 errLen += part.length;64 });65 const finish = (code: number | null): void => {66 if (done) return;67 done = true;68 clearTimeout(timer);69 resolve({70 code,71 stdout: Buffer.concat(out).toString('utf8'),72 stderr: Buffer.concat(err).toString('utf8'),73 truncated,74 timedOut,75 spawnError,76 });77 };78 child.on('error', (e: NodeJS.ErrnoException) => {79 spawnError = e.code ?? 'spawn_failed';80 finish(null);81 });82 child.on('close', (code) => finish(code));83 });84}85