68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
import { spawn } from 'node:child_process';
|
|
import type { ChildProcess } from 'node:child_process';
|
|
|
|
import type { AdapterOutcome, AdapterRequest, ProcessAdapter } from './types.js';
|
|
|
|
/**
|
|
* Default thin process adapter (spawn-based). Runs a command to completion with
|
|
* a hard timeout and reports exit code + captured output — it owns NO verdict
|
|
* logic. Interpreting the outcome is always the check implementation's job.
|
|
*/
|
|
export function createSpawnProcessAdapter(): ProcessAdapter {
|
|
return {
|
|
run(request: AdapterRequest): Promise<AdapterOutcome> {
|
|
return new Promise((resolve) => {
|
|
let child: ChildProcess;
|
|
try {
|
|
child = spawn(request.file, request.args, {
|
|
cwd: request.cwd,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
} catch (error) {
|
|
resolve({
|
|
ok: false,
|
|
kind: 'spawn-error',
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return;
|
|
}
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let settled = false;
|
|
|
|
const timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
child.kill('SIGKILL');
|
|
resolve({
|
|
ok: false,
|
|
kind: 'timeout',
|
|
message: `process timed out after ${request.timeoutMs}ms: ${request.file}`,
|
|
});
|
|
}, request.timeoutMs);
|
|
|
|
const settle = (outcome: AdapterOutcome): void => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
resolve(outcome);
|
|
};
|
|
|
|
child.stdout?.on('data', (chunk: Buffer) => {
|
|
stdout += chunk.toString('utf8');
|
|
});
|
|
child.stderr?.on('data', (chunk: Buffer) => {
|
|
stderr += chunk.toString('utf8');
|
|
});
|
|
child.on('error', (error: Error) => {
|
|
settle({ ok: false, kind: 'spawn-error', message: error.message });
|
|
});
|
|
child.on('close', (code: number | null) => {
|
|
settle({ ok: true, exitCode: code, stdout, stderr });
|
|
});
|
|
});
|
|
},
|
|
};
|
|
}
|