From 91c846bee00e3bd2a82977a1fa7b321a63ea21d0 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma Date: Thu, 26 Feb 2026 04:50:29 +0100 Subject: [PATCH] fix(browser): clear stale Chrome singleton lock before launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an MCP client (or the host process) crashes or is killed without cleanly shutting down the browser, Chrome's singleton lock files (SingletonLock, SingletonSocket, SingletonCookie) persist in the user data directory. On the next startup Chrome refuses to launch because it believes another instance is already running, producing the error: "The browser is already running for ." This is particularly disruptive for MCP clients that manage the browser lifecycle automatically — users have to manually find and delete the lock files or restart their machine to recover. This commit adds a `clearStaleSingletonLock()` helper that runs before `puppeteer.launch()` when a persistent user data directory is in use. It reads the PID encoded in the SingletonLock symlink (format: `hostname-PID`), checks whether that process is still alive via `process.kill(pid, 0)`, and removes the lock files only when the process no longer exists. If the process is still running the lock is left untouched and Chrome's own conflict detection handles it normally. --- src/browser.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/browser.ts b/src/browser.ts index 3fb79a05e..233381fc5 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -167,6 +167,45 @@ export function detectDisplay(): void { } } +/** + * Removes stale Chrome singleton lock files from a user data directory. + * + * When an MCP client crashes without cleanly shutting down the browser, + * Chrome's singleton lock files (SingletonLock, SingletonSocket, + * SingletonCookie) remain on disk. On the next startup, Chrome refuses to + * launch because it thinks an instance is already running. This function + * checks whether the process referenced by SingletonLock is still alive and, + * if not, removes the stale lock files so Chrome can start cleanly. + */ +async function clearStaleSingletonLock(userDataDir: string): Promise { + const lockPath = path.join(userDataDir, 'SingletonLock'); + let lockTarget: string; + try { + lockTarget = await fs.promises.readlink(lockPath); + } catch { + return; // No lock file present, nothing to do. + } + + const pid = parseInt(lockTarget.split('-').at(-1) ?? '', 10); + if (isNaN(pid)) { + return; + } + + try { + process.kill(pid, 0); // Throws if the process does not exist. + return; // Process is alive, leave the lock alone. + } catch { + // Process is gone — the lock is stale. + } + + logger(`Removing stale Chrome singleton lock for PID ${pid} in ${userDataDir}`); + await Promise.all([ + fs.promises.unlink(lockPath).catch(() => {}), + fs.promises.unlink(path.join(userDataDir, 'SingletonSocket')).catch(() => {}), + fs.promises.unlink(path.join(userDataDir, 'SingletonCookie')).catch(() => {}), + ]); +} + export async function launch(options: McpLaunchOptions): Promise { const {channel, executablePath, headless, isolated} = options; const profileDirName = @@ -212,6 +251,10 @@ export async function launch(options: McpLaunchOptions): Promise { detectDisplay(); } + if (userDataDir) { + await clearStaleSingletonLock(userDataDir); + } + try { const browser = await puppeteer.launch({ channel: puppeteerChannel,