feat(baoyu-chrome-cdp): add gracefulKillChrome and fix killChrome process state check

This commit is contained in:
Jim Liu 宝玉 2026-04-01 02:11:51 -05:00
parent 8e2967d4a2
commit 60ab574559
2 changed files with 87 additions and 1 deletions

View File

@ -11,6 +11,7 @@ import {
discoverRunningChromeDebugPort, discoverRunningChromeDebugPort,
findChromeExecutable, findChromeExecutable,
findExistingChromeDebugPort, findExistingChromeDebugPort,
gracefulKillChrome,
getFreePort, getFreePort,
openPageSession, openPageSession,
resolveSharedChromeProfileDir, resolveSharedChromeProfileDir,
@ -110,6 +111,44 @@ async function stopProcess(child: ChildProcess | null): Promise<void> {
await new Promise((resolve) => child.once("exit", resolve)); await new Promise((resolve) => child.once("exit", resolve));
} }
async function startPortHoldingProcess(port: number): Promise<ChildProcess> {
const child = spawn(
process.execPath,
[
"-e",
`
const http = require("node:http");
const port = Number(process.argv[1]);
const server = http.createServer((_req, res) => res.end("ok"));
server.listen(port, "127.0.0.1", () => process.stdout.write("ready\\n"));
setInterval(() => {}, 1000);
`,
String(port),
],
{
stdio: ["ignore", "pipe", "ignore"],
},
);
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("Timed out waiting for child server to start.")), 3_000);
child.once("error", (error) => {
clearTimeout(timer);
reject(error);
});
child.stdout?.once("data", () => {
clearTimeout(timer);
resolve();
});
child.once("exit", () => {
clearTimeout(timer);
reject(new Error("Child server exited before becoming ready."));
});
});
return child;
}
test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => { test("getFreePort honors a fixed environment override and otherwise allocates a TCP port", async (t) => {
useEnv(t, { TEST_FIXED_PORT: "45678" }); useEnv(t, { TEST_FIXED_PORT: "45678" });
assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678); assert.equal(await getFreePort("TEST_FIXED_PORT"), 45678);
@ -305,3 +344,19 @@ test("waitForChromeDebugPort retries until the debug endpoint becomes available"
assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`); assert.equal(websocketUrl, `ws://127.0.0.1:${port}/devtools/browser/demo`);
}); });
test("gracefulKillChrome waits for the Chrome process to exit and release its port", async (t) => {
const port = await getFreePort();
const child = await startPortHoldingProcess(port);
t.after(async () => { await stopProcess(child); });
assert.equal(await waitForChromeDebugPort(port, 1_000).catch(() => null), null);
await gracefulKillChrome(child, port, 4_000);
assert.ok(child.exitCode !== null || child.signalCode !== null);
assert.equal(
await fetch(`http://127.0.0.1:${port}`).then(() => true).catch(() => false),
false,
);
});

View File

@ -478,7 +478,7 @@ export function killChrome(chrome: ChildProcess): void {
chrome.kill("SIGTERM"); chrome.kill("SIGTERM");
} catch {} } catch {}
setTimeout(() => { setTimeout(() => {
if (!chrome.killed) { if (chrome.exitCode === null && chrome.signalCode === null) {
try { try {
chrome.kill("SIGKILL"); chrome.kill("SIGKILL");
} catch {} } catch {}
@ -486,6 +486,37 @@ export function killChrome(chrome: ChildProcess): void {
}, 2_000).unref?.(); }, 2_000).unref?.();
} }
export async function gracefulKillChrome(
chrome: ChildProcess,
port?: number,
timeoutMs = 6_000,
): Promise<void> {
if (chrome.exitCode !== null || chrome.signalCode !== null) return;
const exitPromise = new Promise<void>((resolve) => {
chrome.once("exit", () => resolve());
});
killChrome(chrome);
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (chrome.exitCode !== null || chrome.signalCode !== null) return;
if (port !== undefined && !await isPortListening(port, 250)) return;
const exited = await Promise.race([
exitPromise.then(() => true),
sleep(100).then(() => false),
]);
if (exited) return;
}
await Promise.race([
exitPromise,
sleep(250),
]);
}
export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> { export async function openPageSession(options: OpenPageSessionOptions): Promise<PageSession> {
let targetId: string; let targetId: string;
let createdTarget = false; let createdTarget = false;