This commit is contained in:
seb
2026-10-07 22:10:24 +02:00
parent d49ec77043
commit 3d1601595e
2 changed files with 71 additions and 6 deletions

View File

@@ -65,7 +65,12 @@ function startProc(cmd, procArgs, env, tag) {
cwd: FRONT_ROOT,
env: { ...process.env, ...env },
stdio: ['ignore', 'pipe', 'pipe'],
// Own process group per child so stopProc can kill the whole tree
// (e.g. `npx vite` wrapper + the real vite process).
detached: process.platform !== 'win32',
});
// Piped stdio keeps the parent alive even after kill(); detach the streams
// so teardown below can actually exit.
child.stdout.on('data', (d) => process.stdout.write(`[${tag}] ${d}`));
child.stderr.on('data', (d) => {
const text = String(d);
@@ -73,9 +78,55 @@ function startProc(cmd, procArgs, env, tag) {
if (tag === 'vite' && text.includes('[vite-proxy]')) return;
process.stderr.write(`[${tag}] ${d}`);
});
child.stdout.unref?.();
child.stderr.unref?.();
child.unref?.();
return child;
}
async function stopProc(child, tag, timeoutMs = 8000) {
if (!child || child.exitCode != null || child.signalCode != null) return;
// Kill the whole process group (child + grandchildren like npx/vite).
const killTree = (sig) => {
try {
if (child.pid && process.platform !== 'win32') {
process.kill(-child.pid, sig);
return;
}
} catch {
// fall through to single kill
}
try {
child.kill(sig);
} catch {
// already gone
}
};
await new Promise((resolve) => {
const timer = setTimeout(() => {
killTree('SIGKILL');
resolve();
}, timeoutMs);
timer.unref?.();
child.once('exit', () => {
clearTimeout(timer);
resolve();
});
killTree('SIGTERM');
});
// Drain any buffered pipe listeners so they can't hold the loop open.
try {
child.stdout?.destroy();
} catch {
// ignore
}
try {
child.stderr?.destroy();
} catch {
// ignore
}
}
async function main() {
if (!fs.existsSync(CHROME)) {
throw new Error(`Chrome not found at ${CHROME} (pass --chrome=...)`);
@@ -135,15 +186,18 @@ async function main() {
}
}
} finally {
chrome.kill();
vite.kill();
dummy.kill();
await stopProc(chrome, 'chrome');
await stopProc(vite, 'vite');
await stopProc(dummy, 'dummy');
}
if (failed > 0) {
console.error(`[capture] ${failed} shots failed`);
process.exit(1);
}
console.log(`[capture] done → ${OUT_DIR}`);
// Explicit exit: spawned grandchildren (vite/esbuild, chrome helpers) can
// otherwise keep the event loop alive after teardown.
process.exit(0);
}
main().catch((err) => {

View File

@@ -89,14 +89,25 @@ class CdpConnection {
send(method, params = {}, timeoutMs = 30000) {
const callId = ++this.id;
return new Promise((resolve, reject) => {
this.pending.set(callId, { resolve, reject });
this.sendFrame(0x1, Buffer.from(JSON.stringify({ id: callId, method, params })));
setTimeout(() => {
const timer = setTimeout(() => {
if (this.pending.has(callId)) {
this.pending.delete(callId);
reject(new Error(`CDP timeout: ${method}`));
}
}, timeoutMs);
// Don't let a settled call's timer hold the process open.
timer.unref?.();
this.pending.set(callId, {
resolve: (v) => {
clearTimeout(timer);
resolve(v);
},
reject: (e) => {
clearTimeout(timer);
reject(e);
},
});
this.sendFrame(0x1, Buffer.from(JSON.stringify({ id: callId, method, params })));
});
}