Skip to content

fix: prevent orphaned browser when client disconnects#222

Open
7sorium wants to merge 1 commit into
executeautomation:mainfrom
7sorium:fix/prevent-orphaned-browser-on-exit
Open

fix: prevent orphaned browser when client disconnects#222
7sorium wants to merge 1 commit into
executeautomation:mainfrom
7sorium:fix/prevent-orphaned-browser-on-exit

Conversation

@7sorium

@7sorium 7sorium commented May 31, 2026

Copy link
Copy Markdown

Problem

When running in stdio mode, the server can leave orphaned Chromium processes behind:

  1. Client/parent disconnect is not handled. StdioServerTransport only attaches data/error listeners to stdin — it never reacts to stdin reaching EOF. So when the MCP client (or parent process) dies, the server keeps running, frequently reparented to launchd/init, still holding its launched browser. These accumulate over time as stale headless browsers eating memory.
  2. Graceful shutdown doesn't close the browser. The SIGINT/SIGTERM handler calls process.exit(0) without closing the browser, leaving cleanup to Playwright's own exit hooks, which is racy under an immediate process.exit.

Fix

  • Add closeBrowser() to toolHandler.ts — closes the active browser (if connected) and resets state; safe to call when no browser is open.
  • Call it from shutdown() before exit, wrapped in Promise.race with a 5s timeout so a hung close can't block exit, plus a re-entrancy guard for repeated signals.
  • Watch process.stdin end/close in stdio mode so the server shuts down cleanly when the client/parent goes away. (HTTP --port mode is unaffected — these listeners are only registered on the stdio path.)

Verification

Launching a browser via playwright_navigate, then closing the server's stdin:

  • Before: server lingers indefinitely, Chromium orphaned.
  • After: server exits and all headless_shell child processes are reaped — no orphans.

All 150 existing tests pass; build is clean.

🤖 Generated with Claude Code

StdioServerTransport only listens for 'data'/'error' on stdin, so when the
parent/client process dies (stdin EOF) nothing tears the server down. The
process lingers (often reparented to launchd/init) holding its Chromium,
which accumulates over time as stale headless browsers.

Additionally, the graceful shutdown handler exits without closing the
browser, so even SIGINT/SIGTERM left Chromium to be cleaned up only by
Playwright's own exit hooks (racy under process.exit).

This change:
- adds closeBrowser() to toolHandler and calls it from shutdown(), with a
  5s timeout so a hung close can't block exit and a re-entrancy guard
- watches process.stdin 'end'/'close' in stdio mode to shut down cleanly
  when the client/parent goes away

Verified: launching a browser then closing the server's stdin now reaps
both the server process and its Chromium children (no orphans).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant