A stdio MCP server's lifeline is stdin: when the host/client goes away, stdin should end and the server should exit. The server paths listened for stdin 'end'/'close' but NOT 'error'. That gap bites with a socket-backed stdin — the shape VS Code / Claude Code use (a socketpair, not a pipe). On client death the socket can surface as an 'error' (ECONNRESET/hangup) instead of a clean 'close'. Unhandled, it escalated to the process-wide uncaughtException handler, which logs and keeps running — so the server orphaned instead of exiting. On Linux a POLLHUP socket fd left registered in epoll then wakes the event loop continuously, pinning a core at 100% CPU; once the main thread spins, the setInterval PPID watchdog can't even fire, so the orphan runs forever (the report's 28+ minutes). Add treatStdinFailureAsShutdown(): listen for 'error' as well as 'end'/'close', and DESTROY the stdin stream on any terminal event so the fd leaves epoll and can't churn, then run the path's shutdown. Wired into the live paths — startDirect, the local-handshake proxy, and StdioTransport — plus the legacy pipe proxy. Fires once (re-entry guard). Note: this is hardening for a class of failure that matches every piece of the report's evidence (socket stdin, userspace main-thread spin, high involuntary context switches, watchdog never firing), but the exact 100% CPU spin could not be reproduced in Docker (Linux) across /dev/null EOF, socket peer-death (RST/FIN), the reporter's 0.9.7 bundle, and the npx chain — all exited cleanly — so the trigger is environment-specific. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d0e649969a
commit
0b1a2eed97
+5
-2
@@ -50,6 +50,7 @@ import {
|
||||
import { connectWithHello, runLocalHandshakeProxy } from './proxy';
|
||||
import { getDaemonSocketPath } from './daemon-paths';
|
||||
import { supervisionLostReason } from './ppid-watchdog';
|
||||
import { treatStdinFailureAsShutdown } from './stdin-teardown';
|
||||
import { HOST_PPID_ENV } from '../extraction/wasm-runtime-flags';
|
||||
|
||||
/**
|
||||
@@ -330,8 +331,10 @@ export class MCPServer {
|
||||
// Detect parent-process death — same logic as pre-refactor. When stdin
|
||||
// closes we go through StdioTransport's `process.exit(0)` already, but
|
||||
// SIGKILL of the parent doesn't reliably close stdin on Linux (#277).
|
||||
process.stdin.on('end', () => this.stop());
|
||||
process.stdin.on('close', () => this.stop());
|
||||
// Also treat a stdin `'error'` (a socket-backed stdin can fail with
|
||||
// ECONNRESET/hangup instead of a clean close) as shutdown, and destroy the
|
||||
// stream so a hung fd can't busy-spin the event loop (#799).
|
||||
treatStdinFailureAsShutdown(() => this.stop());
|
||||
|
||||
this.mode = 'direct';
|
||||
this.installSignalHandlers();
|
||||
|
||||
Reference in New Issue
Block a user