UNPKG

@sentzunhat/zacatl

Version:

A modular, high-performance TypeScript microservice framework for Node.js, featuring layered architecture, dependency injection, and robust validation for building scalable APIs and distributed systems.

97 lines (96 loc) 3.56 kB
import { spawn } from 'child_process'; /** Grace period between SIGTERM and SIGKILL when a timeout fires. */ const SIGKILL_GRACE_MS = 2000; /** * Executes a single shell-free command via `child_process.spawn` (scripts-only). * * Simplified version designed for build scripts that cannot depend on src/. * For production code, use `runCommand` from `src/utils/command-runner`. * * Guarantees (in order): * 1. Hard timeout: SIGTERM → SIGKILL after `SIGKILL_GRACE_MS` grace period * 2. Output capped at `policy.maxOutputBytes` (combined stdout + stderr) * 3. Timing measurement and structured result * * The returned promise always **resolves** — it never rejects. * * @param spec - Structured command to execute * @param policy - Runner policy with timeout and output limits * @returns - Structured `CommandResult` with exit code, output, and timing * * @example * ```typescript * const policy = { timeoutMs: 30000, maxOutputBytes: 1048576, maxConcurrency: 4, inheritEnv: true }; * const result = await runCommand({ cmd: 'node', args: ['--version'] }, policy); * console.log(result.stdout); // v24.x.x * ``` */ export const runCommand = (spec, policy) => { return new Promise((resolve) => { const startMs = Date.now(); let stdout = ''; let stderr = ''; let timedOut = false; let outputBytes = 0; const env = policy.inheritEnv ? { ...process.env, ...(spec.env ?? {}) } : { ...(spec.env ?? {}) }; const child = spawn(spec.cmd, spec.args, { shell: false, cwd: spec.cwd, env, stdio: ['ignore', 'pipe', 'pipe'], }); // — Timeout enforcement const killTimer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); // Escalate to SIGKILL if the process ignores SIGTERM const forceKill = setTimeout(() => { child.kill('SIGKILL'); }, SIGKILL_GRACE_MS); // Keep the event loop from staying alive just for this timer if (typeof forceKill.unref === 'function') forceKill.unref(); }, policy.timeoutMs); // — Output collection (capped at maxOutputBytes) child.stdout?.on('data', (chunk) => { outputBytes += chunk.byteLength; if (outputBytes <= policy.maxOutputBytes) { stdout += chunk.toString('utf-8'); } }); child.stderr?.on('data', (chunk) => { outputBytes += chunk.byteLength; if (outputBytes <= policy.maxOutputBytes) { stderr += chunk.toString('utf-8'); } }); // — Normal exit child.on('close', (code) => { clearTimeout(killTimer); resolve({ cmd: spec.cmd, args: spec.args, exitCode: code ?? null, stdout, stderr, timedOut, durationMs: Date.now() - startMs, }); }); // — Spawn / OS errors (e.g. ENOENT — binary not found) child.on('error', (err) => { clearTimeout(killTimer); resolve({ cmd: spec.cmd, args: spec.args, exitCode: null, stdout, stderr: stderr ? `${stderr}\n${err.message}` : err.message, timedOut, durationMs: Date.now() - startMs, }); }); }); };