Files
sprites/tools/build/exec.ts
Christopher Monsanto edda6cb310 Implement tools/build core and Buildfile.ts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 22:01:04 -04:00

79 lines
2.6 KiB
TypeScript

import {spawn} from 'child_process';
export interface ExecResult {
code : number | null;
signal : NodeJS.Signals | null;
output : string;
durationMs : number;
}
// The command script is fed to sh via stdin rather than -c: a single argv
// entry is capped by the kernel (MAX_ARG_STRLEN, ~128KB) and the largest %f
// expansion is already 80KB+.
export function runShell(command : string, opts : {cwd : string, signal : AbortSignal}) : Promise<ExecResult> {
return new Promise((resolve, reject) => {
const start = performance.now();
// detached: own process group, so an abort kills grandchildren
// (magick, optipng, ...) with one signal
const child = spawn('sh', [], {cwd: opts.cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe']});
const chunks : Buffer[] = [];
child.stdout.on('data', c => chunks.push(c));
child.stderr.on('data', c => chunks.push(c));
child.stdin.on('error', () => {}); // EPIPE if the shell exits early
child.stdin.end(command + '\n');
let killTimer : NodeJS.Timeout | undefined;
const kill = (sig : NodeJS.Signals) => {
try {
process.kill(-child.pid!, sig);
} catch {}
};
const onAbort = () => {
kill('SIGTERM');
killTimer = setTimeout(() => kill('SIGKILL'), 5000);
killTimer.unref();
};
if (opts.signal.aborted) {
onAbort();
} else {
opts.signal.addEventListener('abort', onAbort, {once: true});
}
const cleanup = () => {
opts.signal.removeEventListener('abort', onAbort);
if (killTimer !== undefined) {
clearTimeout(killTimer);
}
};
child.on('error', err => {
cleanup();
reject(err);
});
child.on('close', (code, signal) => {
cleanup();
resolve({
code,
signal,
output: Buffer.concat(chunks).toString(),
durationMs: performance.now() - start,
});
});
});
}
export async function workerPool<T>(items : readonly T[], jobs : number,
fn : (item : T, index : number) => Promise<void>) : Promise<void> {
let next = 0;
const workers = [];
for (let i = 0; i < Math.max(1, Math.min(jobs, items.length)); i++) {
workers.push((async () => {
while (next < items.length) {
const index = next++;
await fn(items[index]!, index);
}
})());
}
await Promise.all(workers);
}