v1.87.2.0 fix: preserve headed browsers during headless cleanup (#2874)

* fix(browse): isolate headless lifecycle from shared profiles

* test(cso): let Windows integration finish within subprocess budgets

* docs: format browser isolation release summary

* docs: update browser isolation documentation for v1.87.2.0

---------

Co-authored-by: garrytan <19957+garrytan@users.noreply.github.com>
This commit is contained in:
Garry Tan
2026-09-15 20:16:07 +00:00
committed by GitHub
co-authored by garrytan
parent 6b09a582eb
commit 43c9e45ea7
10 changed files with 300 additions and 23 deletions
+13 -6
View File
@@ -515,8 +515,10 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
// holding it) before launch, so an auto-restart after an abrupt kill isn't
// blocked by the previous Chromium's SingletonLock — the self-inflicted
// crash-loop. Previously only the manual connect preamble did this.
await killOrphanChromium();
cleanChromiumProfileLocks();
if ((extraEnv?.BROWSE_HEADED ?? process.env.BROWSE_HEADED) === '1') {
await killOrphanChromium();
cleanChromiumProfileLocks();
}
// Allow the caller to opt out of the parent-process watchdog by setting
// BROWSE_PARENT_PID=0 in the environment. Useful for CI, non-interactive
@@ -1818,8 +1820,11 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
// #1781: killing the daemon can orphan its Chromium child tree, which keeps
// holding the SingletonLock and makes the next `connect` fail to launch.
// Reap the orphan via the lock, then clear the lock files + state.
await killOrphanChromium();
cleanChromiumProfileLocks();
if (existingState.mode === 'headed') {
await killOrphanChromium();
cleanChromiumProfileLocks();
}
await reapRecordedChromium(existingState);
// Xvfb orphan cleanup: if the recorded PID still matches our Xvfb (by
// cmdline AND start-time), kill it. PID-only would risk killing a
// recycled PID belonging to an unrelated process.
@@ -1876,9 +1881,11 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
// NEXT launch is clean (same cleanup as the disconnect force path).
// The headless child has no SingletonLock — reap it via the recorded
// identity too (#2709).
await killOrphanChromium();
if (stopState.mode === 'headed') {
await killOrphanChromium();
cleanChromiumProfileLocks();
}
await reapRecordedChromium(stopState);
cleanChromiumProfileLocks();
safeUnlinkQuiet(config.stateFile);
console.log('Daemon stopped (forced — tabs/cookies/logins discarded).');
process.exit(0);
+6 -2
View File
@@ -1469,7 +1469,9 @@ function emergencyCleanup() {
// Clean Chromium profile locks via the shared helper (defensive guard
// refuses to operate on unrecognized profile dirs).
cleanSingletonLocks(resolveChromiumProfile());
if (activeBrowserManager.getConnectionMode() === 'headed' || process.env.BROWSE_HEADED === '1') {
cleanSingletonLocks(resolveChromiumProfile());
}
safeUnlinkQuiet(config.stateFile);
}
// Same import.meta.main gate as SIGINT/SIGTERM — embedders register their
@@ -1704,7 +1706,9 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
await cfgBrowserManager.close();
cleanSingletonLocks(resolveChromiumProfile());
if (cfgBrowserManager.getConnectionMode() === 'headed') {
cleanSingletonLocks(resolveChromiumProfile());
}
safeUnlinkQuiet(config.stateFile);
process.exit(exitCode);
}