-
Notifications
You must be signed in to change notification settings - Fork 8
fix: prevent orphaned processes via stdin/ppid/onclose lifecycle guards #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1612,9 +1612,57 @@ async function main() { | |
| } | ||
| } | ||
|
|
||
| // Parent death guard — catches SIGKILL, crashes, terminal close on ALL platforms. | ||
| // process.kill(pid, 0) throws ESRCH when the process no longer exists. | ||
| const parentPid = process.ppid; | ||
| if (parentPid > 1) { | ||
| const parentGuard = setInterval(() => { | ||
| try { | ||
| process.kill(parentPid, 0); | ||
| } catch (err: unknown) { | ||
| // ESRCH = process gone → exit. EPERM = process alive, different UID → ignore. | ||
| if ((err as NodeJS.ErrnoException).code === 'ESRCH') { | ||
| process.exit(0); | ||
| } | ||
| } | ||
| }, 5_000); | ||
| parentGuard.unref(); | ||
| } | ||
|
|
||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
|
|
||
| // Register cleanup before any handler that calls process.exit(), so the | ||
| // exit listener is always in place when stdin/onclose/signals fire. | ||
| const stopAllWatchers = () => { | ||
| for (const project of getAllProjects()) { | ||
| project.stopWatcher?.(); | ||
| } | ||
| }; | ||
|
|
||
| process.once('exit', stopAllWatchers); | ||
| process.once('SIGINT', () => { | ||
| stopAllWatchers(); | ||
| process.exit(0); | ||
| }); | ||
| process.once('SIGTERM', () => { | ||
| stopAllWatchers(); | ||
| process.exit(0); | ||
| }); | ||
| process.once('SIGHUP', () => { | ||
| stopAllWatchers(); | ||
| process.exit(0); | ||
| }); | ||
|
|
||
| // Detect stdin pipe closure — the primary signal that the MCP client is gone. | ||
| // StdioServerTransport only listens for 'data'/'error', never 'end'. | ||
| process.stdin.on('end', () => process.exit(0)); | ||
| process.stdin.on('close', () => process.exit(0)); | ||
|
|
||
| // Handle graceful MCP protocol-level disconnect. | ||
| // Fires after SDK internal cleanup when transport.close() is called. | ||
| server.onclose = () => process.exit(0); | ||
|
Comment on lines
+1657
to
+1664
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The If either handler fires after One safe pattern is to hoist the |
||
|
|
||
| if (process.env.CODEBASE_CONTEXT_DEBUG) console.error('[DEBUG] Server ready'); | ||
|
|
||
| await refreshKnownRootsFromClient(); | ||
|
|
@@ -1634,23 +1682,6 @@ async function main() { | |
| /* best-effort */ | ||
| } | ||
| }); | ||
|
|
||
| // Cleanup all watchers on exit | ||
| const stopAllWatchers = () => { | ||
| for (const project of getAllProjects()) { | ||
| project.stopWatcher?.(); | ||
| } | ||
| }; | ||
|
|
||
| process.once('exit', stopAllWatchers); | ||
| process.once('SIGINT', () => { | ||
| stopAllWatchers(); | ||
| process.exit(0); | ||
| }); | ||
| process.once('SIGTERM', () => { | ||
| stopAllWatchers(); | ||
| process.exit(0); | ||
| }); | ||
| } | ||
|
|
||
| // Export server components for programmatic use | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EPERM, falsely kills serverprocess.kill(pid, 0)throws two distinct errors:ESRCH— process does not exist → correct to exitEPERM— process exists but we lack permission to signal it → incorrect to exitThe bare
catchblock catches both, meaning if the parent process is still alive but running in a slightly different permission context (e.g., after asetuidcall, or on certain Linux configurations where a parent can have a different UID), the server will exit spuriously every 5 seconds.The fix is to only exit on
ESRCH: