u
This commit is contained in:
@@ -65,7 +65,12 @@ function startProc(cmd, procArgs, env, tag) {
|
|||||||
cwd: FRONT_ROOT,
|
cwd: FRONT_ROOT,
|
||||||
env: { ...process.env, ...env },
|
env: { ...process.env, ...env },
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
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.stdout.on('data', (d) => process.stdout.write(`[${tag}] ${d}`));
|
||||||
child.stderr.on('data', (d) => {
|
child.stderr.on('data', (d) => {
|
||||||
const text = String(d);
|
const text = String(d);
|
||||||
@@ -73,9 +78,55 @@ function startProc(cmd, procArgs, env, tag) {
|
|||||||
if (tag === 'vite' && text.includes('[vite-proxy]')) return;
|
if (tag === 'vite' && text.includes('[vite-proxy]')) return;
|
||||||
process.stderr.write(`[${tag}] ${d}`);
|
process.stderr.write(`[${tag}] ${d}`);
|
||||||
});
|
});
|
||||||
|
child.stdout.unref?.();
|
||||||
|
child.stderr.unref?.();
|
||||||
|
child.unref?.();
|
||||||
return child;
|
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() {
|
async function main() {
|
||||||
if (!fs.existsSync(CHROME)) {
|
if (!fs.existsSync(CHROME)) {
|
||||||
throw new Error(`Chrome not found at ${CHROME} (pass --chrome=...)`);
|
throw new Error(`Chrome not found at ${CHROME} (pass --chrome=...)`);
|
||||||
@@ -135,15 +186,18 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
chrome.kill();
|
await stopProc(chrome, 'chrome');
|
||||||
vite.kill();
|
await stopProc(vite, 'vite');
|
||||||
dummy.kill();
|
await stopProc(dummy, 'dummy');
|
||||||
}
|
}
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
console.error(`[capture] ${failed} shots failed`);
|
console.error(`[capture] ${failed} shots failed`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log(`[capture] done → ${OUT_DIR}`);
|
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) => {
|
main().catch((err) => {
|
||||||
|
|||||||
17
test/cdp.mjs
17
test/cdp.mjs
@@ -89,14 +89,25 @@ class CdpConnection {
|
|||||||
send(method, params = {}, timeoutMs = 30000) {
|
send(method, params = {}, timeoutMs = 30000) {
|
||||||
const callId = ++this.id;
|
const callId = ++this.id;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.pending.set(callId, { resolve, reject });
|
const timer = setTimeout(() => {
|
||||||
this.sendFrame(0x1, Buffer.from(JSON.stringify({ id: callId, method, params })));
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.pending.has(callId)) {
|
if (this.pending.has(callId)) {
|
||||||
this.pending.delete(callId);
|
this.pending.delete(callId);
|
||||||
reject(new Error(`CDP timeout: ${method}`));
|
reject(new Error(`CDP timeout: ${method}`));
|
||||||
}
|
}
|
||||||
}, timeoutMs);
|
}, 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