Skip to content

fix: clean stale lock files before launchPersistentContext - #320

Open
dyiapanis wants to merge 2 commits into
apify:masterfrom
dyiapanis:fix/stale-lock-cleanup
Open

fix: clean stale lock files before launchPersistentContext#320
dyiapanis wants to merge 2 commits into
apify:masterfrom
dyiapanis:fix/stale-lock-cleanup

Conversation

@dyiapanis

Copy link
Copy Markdown

When a Camoufox browser process crashes (SIGSEGV, OOM, unclean exit), Firefox leaves and files in the user data directory. These prevent a new browser from launching for the same profile — the launch appears to succeed but the browser immediately closes with 'persistent context closed' and no crash trace.

Add that removes stale lock files
before is called. Safe to call even if no
lock files exist (no-op).

Discovered while running concurrent browser instances for web extraction (Centipede plugin). After a SIGSEGV crash, the affected profile directory blocks all future launches for that userId until lock files are manually removed.

When a Camoufox browser process crashes (SIGSEGV, OOM, unclean exit),
Firefox leaves  and  files in the user data directory.
These prevent a new browser from launching for the same profile — the
launch appears to succeed but the browser immediately closes with
'persistent context closed' and no crash trace.

Add  that removes stale lock files
before  is called. Safe to call even if no
lock files exist (no-op).

Discovered while running concurrent browser instances for web
extraction (Centipede plugin). After a SIGSEGV crash, the affected
profile directory blocks all future launches for that userId until
lock files are manually removed.
@barjin

barjin commented Aug 3, 2026

Copy link
Copy Markdown
Member

Thank you for your contribution to this project @dyiapanis.

existsSync() follows symlinks, and Firefox's lock is a symlink to a nonexistent <ip>:+<pid>:

$ ln -s '127.0.0.1:+12345' lock
fs.existsSync('lock')  // false
fs.lstatSync('lock')   // ok

so the main artifact is never removed (utils.ts:433) — needs lstatSync, or just rmSync(p, { force: true }). Removal is also unconditional: unlinking .parentlock doesn't release a live Firefox's fcntl lock, so the next launch creates a fresh inode, locks that, and two instances share one profile. The target encodes hostname:pid - worth checking the pid is dead first, and making this opt-in.

Cheers!

Per review feedback:
- existsSync() follows symlinks; Firefox 'lock' is a symlink to a
  nonexistent target, so it never matched. Switch to lstatSync.
- Parse the pid from the symlink target (<host>:+<pid>) and check
  process.kill(pid, 0) before removing — never delete a live process's
  lock (causes shared profile corruption).
- Use rmSync(force) instead of existsSync+unlinkSync.
- Make cleanup opt-in via clean_stale_locks: true in LaunchOptions.
@dyiapanis

Copy link
Copy Markdown
Author

Thanks for the review — all three points addressed in 5dc4d1e:

1. existsSync() follows symlinks — Switched to lstatSync which doesn't follow symlinks. Firefox's lock is a symlink to <host>:+<pid>, so existsSync returned false and the lock was never detected. lstatSync correctly identifies the symlink.

2. Unconditional removal — Now parse the pid from the symlink target (<host>:+<pid> format) and check process.kill(pid, 0) before removing. If the pid is still alive (ESRCH not thrown), we skip the lock. EPERM (process exists but not ours) is treated as alive. Only dead-process locks are removed.

3. Opt-in — Added clean_stale_locks: boolean to LaunchOptions. The cleanup only runs when the user explicitly sets clean_stale_locks: true. Default is off.

Also switched from existsSync + unlinkSync to rmSync(path, { force: true }) which handles symlinks robustly.

@dyiapanis
dyiapanis force-pushed the fix/stale-lock-cleanup branch from 5dc4d1e to d49985a Compare August 4, 2026 07:52
@dyiapanis

Copy link
Copy Markdown
Author

Hi @barjin — following up on this. All three points from your review were addressed in 5dc4d1e (switched to lstatSync, added pid liveness check via process.kill(pid, 0), and guarded the removal with try/catch). Let me know if the changes look good or if there's anything else you'd like adjusted.

1 similar comment
@dyiapanis

Copy link
Copy Markdown
Author

Hi @barjin — following up on this. All three points from your review were addressed in 5dc4d1e (switched to lstatSync, added pid liveness check via process.kill(pid, 0), and guarded the removal with try/catch). Let me know if the changes look good or if there's anything else you'd like adjusted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants