| 117 | interface OnceResult { exitCode: number; stdout: string; stderr: string } |
| 118 | |
| 119 | function runOnce(cmd: string, args: string[], cwd: string, signal?: AbortSignal, timeoutMs = 30_000): Promise<OnceResult> { |
| 120 | return new Promise<OnceResult>((resolve) => { |
| 121 | let proc: ReturnType<typeof spawn>; |
| 122 | try { |
| 123 | proc = spawn(cmd, args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] }); |
| 124 | } catch (e: any) { |
| 125 | resolve({ exitCode: 127, stdout: '', stderr: e.message }); |
| 126 | return; |
| 127 | } |
| 128 | let stdout = ''; |
| 129 | let stderr = ''; |
| 130 | let settled = false; |
| 131 | const done = (r: OnceResult): void => { if (!settled) { settled = true; clearTimeout(t); clearTimeout(k); resolve(r); } }; |
| 132 | proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString(); }); |
| 133 | proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString(); }); |
| 134 | const t = setTimeout(() => { try { proc.kill('SIGTERM'); } catch {} }, timeoutMs); |
| 135 | const k = setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, timeoutMs + 2000); |
| 136 | proc.on('close', (code) => done({ exitCode: code ?? 130, stdout, stderr })); |
| 137 | proc.on('error', (e: any) => { |
| 138 | if (e?.code === 'ENOENT') { |
| 139 | done({ exitCode: 127, stdout: '', stderr: `${cmd} not found in PATH` }); |
| 140 | } else { |
| 141 | done({ exitCode: 1, stdout, stderr: stderr + (e?.message ?? '') }); |
| 142 | } |
| 143 | }); |
| 144 | }); |
| 145 | } |