Skip to content

Commit 91c846b

Browse files
committed
fix(browser): clear stale Chrome singleton lock before launch
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 <userDataDir>." 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.
1 parent 080bcf6 commit 91c846b

1 file changed

Lines changed: 43 additions & 0 deletions

File tree

src/browser.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,45 @@ export function detectDisplay(): void {
167167
}
168168
}
169169

170+
/**
171+
* Removes stale Chrome singleton lock files from a user data directory.
172+
*
173+
* When an MCP client crashes without cleanly shutting down the browser,
174+
* Chrome's singleton lock files (SingletonLock, SingletonSocket,
175+
* SingletonCookie) remain on disk. On the next startup, Chrome refuses to
176+
* launch because it thinks an instance is already running. This function
177+
* checks whether the process referenced by SingletonLock is still alive and,
178+
* if not, removes the stale lock files so Chrome can start cleanly.
179+
*/
180+
async function clearStaleSingletonLock(userDataDir: string): Promise<void> {
181+
const lockPath = path.join(userDataDir, 'SingletonLock');
182+
let lockTarget: string;
183+
try {
184+
lockTarget = await fs.promises.readlink(lockPath);
185+
} catch {
186+
return; // No lock file present, nothing to do.
187+
}
188+
189+
const pid = parseInt(lockTarget.split('-').at(-1) ?? '', 10);
190+
if (isNaN(pid)) {
191+
return;
192+
}
193+
194+
try {
195+
process.kill(pid, 0); // Throws if the process does not exist.
196+
return; // Process is alive, leave the lock alone.
197+
} catch {
198+
// Process is gone — the lock is stale.
199+
}
200+
201+
logger(`Removing stale Chrome singleton lock for PID ${pid} in ${userDataDir}`);
202+
await Promise.all([
203+
fs.promises.unlink(lockPath).catch(() => {}),
204+
fs.promises.unlink(path.join(userDataDir, 'SingletonSocket')).catch(() => {}),
205+
fs.promises.unlink(path.join(userDataDir, 'SingletonCookie')).catch(() => {}),
206+
]);
207+
}
208+
170209
export async function launch(options: McpLaunchOptions): Promise<Browser> {
171210
const {channel, executablePath, headless, isolated} = options;
172211
const profileDirName =
@@ -212,6 +251,10 @@ export async function launch(options: McpLaunchOptions): Promise<Browser> {
212251
detectDisplay();
213252
}
214253

254+
if (userDataDir) {
255+
await clearStaleSingletonLock(userDataDir);
256+
}
257+
215258
try {
216259
const browser = await puppeteer.launch({
217260
channel: puppeteerChannel,

0 commit comments

Comments
 (0)