SPB Git

Repository Public

fsx-exec.ts 2.4 KB 85 lines typescript

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
Files main
Menu

Theme