| 50 | * those are reported in the result so the calling tool can format a clean message. |
| 51 | */ |
| 52 | export function runProcess( |
| 53 | bin: string, |
| 54 | args: string[], |
| 55 | opts: RunProcessOptions = {}, |
| 56 | ): Promise<RunProcessResult> { |
| 57 | const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; |
| 58 | const maxOutput = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT; |
| 59 | |
| 60 | return new Promise<RunProcessResult>((resolve) => { |
| 61 | let stdout = ''; |
| 62 | let stderr = ''; |
| 63 | let stdoutBytes = 0; |
| 64 | let stderrBytes = 0; |
| 65 | let timedOut = false; |
| 66 | let settled = false; |
| 67 | |
| 68 | const child = crossSpawn(bin, args, { |
| 69 | cwd: opts.cwd ?? process.cwd(), |
| 70 | env: { ...process.env, ...(opts.env ?? {}) }, |
| 71 | shell: false, |
| 72 | }); |
| 73 | |
| 74 | const finish = (r: Omit<RunProcessResult, 'ok'>) => { |
| 75 | if (settled) return; |
| 76 | settled = true; |
| 77 | clearTimeout(timer); |
| 78 | resolve({ ...r, ok: r.code === 0 && !r.timedOut && !r.notFound }); |
| 79 | }; |
| 80 | |
| 81 | const timer = setTimeout(() => { |
| 82 | timedOut = true; |
| 83 | child.kill('SIGTERM'); |
| 84 | // Escalate if it ignores SIGTERM. |
| 85 | setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* already gone */ } }, 3000); |
| 86 | }, timeoutMs); |
| 87 | |
| 88 | child.stdout?.on('data', (chunk: Buffer) => { |
| 89 | if (stdoutBytes < maxOutput) { |
| 90 | stdout += chunk.toString('utf8'); |
| 91 | stdoutBytes += chunk.length; |
| 92 | if (stdoutBytes >= maxOutput) stdout += '\n…[output truncated]'; |
| 93 | } |
| 94 | }); |
| 95 | child.stderr?.on('data', (chunk: Buffer) => { |
| 96 | if (stderrBytes < maxOutput) { |
| 97 | stderr += chunk.toString('utf8'); |
| 98 | stderrBytes += chunk.length; |
| 99 | if (stderrBytes >= maxOutput) stderr += '\n…[output truncated]'; |
| 100 | } |
| 101 | }); |
| 102 | |
| 103 | child.on('error', (err: NodeJS.ErrnoException) => { |
| 104 | // ENOENT = binary not installed / not on PATH. |
| 105 | if (err.code === 'ENOENT') { |
| 106 | finish({ code: null, stdout, stderr, timedOut: false, notFound: true }); |
| 107 | } else { |
| 108 | finish({ code: null, stdout, stderr: stderr + `\n[spawn error: ${err.message}]`, timedOut, notFound: false }); |
| 109 | } |