u
This commit is contained in:
@@ -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) => {
|
||||
|
||||
17
test/cdp.mjs
17
test/cdp.mjs
@@ -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 })));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user