fix(cli): a killed browser on a busy Windows machine no longer fails check (#5607)

This commit is contained in:
Miguel Ángel
2026-10-14 16:59:25 +00:00
committed by GitHub
parent 7744692a46
commit fe29f157f9
2 changed files with 40 additions and 1 deletions
+30
View File
@@ -0,0 +1,30 @@
import { spawn } from "node:child_process";
import { EventEmitter, once } from "node:events";
import type { Browser } from "puppeteer-core";
import { describe, expect, it } from "vitest";
import { closeManagedBrowser } from "./launch.js";
/** A browser whose close never finishes and whose process never reports 'exit', like a loaded Windows host. */
function silentBrowser(pid: number): Browser {
const child = Object.assign(new EventEmitter(), {
pid,
exitCode: null,
signalCode: null,
kill: () => true,
});
return { process: () => child, close: () => new Promise(() => {}) } as unknown as Browser;
}
describe("closing a killed browser", () => {
it("finishes once the OS reports the process gone, before Node reports its exit", async () => {
const gone = spawn(process.execPath, ["-e", ""]);
await once(gone, "exit");
await expect(closeManagedBrowser(silentBrowser(gone.pid!))).resolves.toBeUndefined();
});
it("still fails when the process is alive after the forced shutdown", async () => {
await expect(closeManagedBrowser(silentBrowser(process.pid))).rejects.toThrow(
"Browser process did not exit after forced shutdown.",
);
});
});
+10 -1
View File
@@ -6,6 +6,7 @@ import {
} from "./gpuPolicy.js";
import { normalizeErrorMessage } from "../utils/errorMessage.js";
import { requestCliExit } from "../utils/commandResult.js";
import { processIsAlive } from "../utils/processTree.js";
export class BrowserCleanupError extends AggregateError {}
@@ -13,6 +14,7 @@ const browsers = new Set<Browser>();
const closing = new WeakMap<Browser, Promise<void>>();
const pending = new Map<Promise<Browser>, AbortController>();
const CLOSE_GRACE_MS = 1_000;
const EXIT_POLL_MS = 50;
let listening = false;
let shutdown: Promise<void> | undefined;
@@ -125,6 +127,7 @@ async function closeBrowser(browser: Browser): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) return;
const exited = new Promise<void>((resolve) => child.once("exit", () => resolve()));
let timer: NodeJS.Timeout | undefined;
let poll: NodeJS.Timeout | undefined;
const grace = new Promise<void>((resolve) => {
timer = setTimeout(resolve, CLOSE_GRACE_MS);
});
@@ -141,8 +144,14 @@ async function closeBrowser(browser: Browser): Promise<void> {
CLOSE_GRACE_MS,
);
});
await Promise.race([exited, exitLimit]);
const goneBeforeExitEvent = new Promise<void>((resolve) => {
poll = setInterval(() => {
if (child.pid === undefined || !processIsAlive(child.pid)) resolve();
}, EXIT_POLL_MS);
});
await Promise.race([exited, goneBeforeExitEvent, exitLimit]);
} finally {
clearTimeout(timer);
clearInterval(poll);
}
}