From 60ab57455911604c740ad02969b063476d5af3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Wed, 1 Apr 2026 02:11:51 -0500 Subject: [PATCH] feat(baoyu-chrome-cdp): add gracefulKillChrome and fix killChrome process state check --- packages/baoyu-chrome-cdp/src/index.test.ts | 55 +++++++++++++++++++++ packages/baoyu-chrome-cdp/src/index.ts | 33 ++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/baoyu-chrome-cdp/src/index.test.ts b/packages/baoyu-chrome-cdp/src/index.test.ts index f745da5..511f450 100644 --- a/packages/baoyu-chrome-cdp/src/index.test.ts +++ b/packages/baoyu-chrome-cdp/src/index.test.ts @@ -11,6 +11,7 @@ import { discoverRunningChromeDebugPort, findChromeExecutable, findExistingChromeDebugPort, + gracefulKillChrome, getFreePort, openPageSession, resolveSharedChromeProfileDir, @@ -110,6 +111,44 @@ async function stopProcess(child: ChildProcess | null): Promise { await new Promise((resolve) => child.once("exit", resolve)); } +async function startPortHoldingProcess(port: number): Promise { + 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((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) => { useEnv(t, { 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`); }); + +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, + ); +}); diff --git a/packages/baoyu-chrome-cdp/src/index.ts b/packages/baoyu-chrome-cdp/src/index.ts index 0be48c3..3d189b1 100644 --- a/packages/baoyu-chrome-cdp/src/index.ts +++ b/packages/baoyu-chrome-cdp/src/index.ts @@ -478,7 +478,7 @@ export function killChrome(chrome: ChildProcess): void { chrome.kill("SIGTERM"); } catch {} setTimeout(() => { - if (!chrome.killed) { + if (chrome.exitCode === null && chrome.signalCode === null) { try { chrome.kill("SIGKILL"); } catch {} @@ -486,6 +486,37 @@ export function killChrome(chrome: ChildProcess): void { }, 2_000).unref?.(); } +export async function gracefulKillChrome( + chrome: ChildProcess, + port?: number, + timeoutMs = 6_000, +): Promise { + if (chrome.exitCode !== null || chrome.signalCode !== null) return; + + const exitPromise = new Promise((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 { let targetId: string; let createdTarget = false;