Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | 40x 40x 40x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 7x 12x 3x 12x 12x 12x 12x 12x 12x 12x 12x 12x 4x 4x 4x 4x 1x 1x 12x 12x 12x 1x | import { spawn, ChildProcess } from 'child_process';
export interface ExecWithKillResult {
stdout: string;
stderr: string;
/** True if the process was killed due to timeout */
killed: boolean;
/** Process exit code (null if killed by signal) */
exitCode: number | null;
}
export interface ExecWithKillOptions {
cwd: string;
timeout: number;
/** Grace period in ms after SIGTERM before sending SIGKILL (default: 5000) */
sigkillGraceMs?: number;
}
const DEFAULT_SIGKILL_GRACE_MS = 5000;
/**
* Executes a command using `spawn` with `detached: true` so the child becomes
* a new process-group leader. On timeout, kills the **entire process group**
* via `process.kill(-pid, 'SIGTERM')`, then falls back to SIGKILL after a
* grace period if the group is still alive.
*
* @param command The shell command string to execute
* @param options Execution options (cwd, timeout, sigkillGraceMs)
* @returns Promise resolving to `{ stdout, stderr, killed, exitCode }`
*/
export function execWithProcessGroupKill(
command: string,
options: ExecWithKillOptions,
): Promise<ExecWithKillResult> {
return new Promise((resolve) => {
const shell = process.env.SHELL || '/bin/bash';
const sigkillGraceMs = options.sigkillGraceMs ?? DEFAULT_SIGKILL_GRACE_MS;
const childProcess: ChildProcess = spawn(shell, ['-c', command], {
cwd: options.cwd,
detached: true, // child becomes new session/process-group leader
stdio: ['pipe', 'pipe', 'pipe'],
});
let killed = false;
let resolved = false;
let extraStderr = '';
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
childProcess.stdout?.on('data', (chunk: Buffer) => {
stdoutChunks.push(chunk);
});
childProcess.stderr?.on('data', (chunk: Buffer) => {
stderrChunks.push(chunk);
});
const doResolve = (exitCode: number | null) => {
Iif (resolved) return;
resolved = true;
clearTimeout(timeoutTimer);
clearTimeout(sigkillTimer);
// Concatenate buffered chunks
const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
const stderr =
Buffer.concat(stderrChunks).toString('utf-8') + extraStderr;
resolve({ stdout, stderr, killed, exitCode });
};
// Timeout handler
let sigkillTimer: ReturnType<typeof setTimeout>;
const timeoutTimer = setTimeout(() => {
killed = true;
try {
// Kill entire process group (negative PID)
process.kill(-childProcess.pid!, 'SIGTERM');
} catch {
// Process may have already exited
}
// Schedule SIGKILL fallback
sigkillTimer = setTimeout(() => {
try {
process.kill(-childProcess.pid!, 'SIGKILL');
} catch {
// Process already dead
}
}, sigkillGraceMs);
}, options.timeout);
// Handle process exit
childProcess.on('close', (code) => {
doResolve(code);
});
// Handle spawn errors (e.g., command not found)
childProcess.on('error', (err) => {
extraStderr += `\nSpawn error: ${err.message}`;
// If there's no close event coming (e.g. ENOENT), resolve now
// Spawn errors on 'error' are followed by 'close' in Node >= 16
// but we resolve on close, so just append the error info
});
});
}
|