Layer-2 defense-in-depth follow-up to the Windows PPID watchdog fix (#711). That fix makes an orphaned proxy exit so its socket closes and the daemon reaps via the refcount + idle timer. This adds two daemon-side safety nets for the residual case where a socket close is never delivered (a Windows named-pipe hazard) and a phantom client would otherwise pin the daemon forever: - Liveness sweep: a proxy now sends an optional client-hello carrying its pid (+ host pid) right after verifying the daemon hello; the daemon periodically drops any client whose peer process is dead, re-arming the idle timer. Fail-safe and version-pinned — a connection that never sends the hello just falls back to the socket-close lifecycle, and the daemon reads it before the transport so a non-hello first line is handed through untouched. - Inactivity backstop: the daemon exits after a generous no-traffic window (CODEGRAPH_DAEMON_MAX_IDLE_MS, default 30 min) even with clients attached, so a phantom client that sends nothing can't keep it alive. Pure helpers (parseClientHelloLine, peerIsDead) are unit-tested; the full handshake + sweep and the backstop are covered end-to-end in mcp-daemon.test.ts. Validated on a real Windows 11 VM: the sweep reaps a dead-pid client over a named pipe and the backstop fires with a client still connected. 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
565eb20e26
commit
80358a84d9
+21
-1
@@ -21,7 +21,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as net from 'net';
|
||||
import { HOST_PPID_ENV } from '../extraction/wasm-runtime-flags';
|
||||
import { DaemonHello, MAX_HELLO_LINE_BYTES } from './daemon';
|
||||
import { DaemonClientHello, DaemonHello, MAX_HELLO_LINE_BYTES } from './daemon';
|
||||
import { supervisionLostReason } from './ppid-watchdog';
|
||||
import { CodeGraphPackageVersion } from './version';
|
||||
import { SERVER_INFO, PROTOCOL_VERSION } from './session';
|
||||
@@ -93,6 +93,7 @@ export async function runProxy(
|
||||
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
|
||||
);
|
||||
|
||||
sendClientHello(socket);
|
||||
startPpidWatchdog(socket);
|
||||
await pipeUntilClose(socket);
|
||||
// Host disconnected (or the daemon went away). The proxy's only job is the
|
||||
@@ -132,9 +133,28 @@ export async function connectWithHello(
|
||||
process.stderr.write(
|
||||
`[CodeGraph MCP] Attached to shared daemon on ${socketPath} (pid ${hello.pid}, v${hello.codegraph}).\n`
|
||||
);
|
||||
sendClientHello(socket);
|
||||
return socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the daemon our pids right after we verify its hello, so its liveness
|
||||
* sweep can reap this client if our process dies without the socket ever
|
||||
* signalling close (the Windows named-pipe hazard behind #692). Best-effort:
|
||||
* sent before any piped bytes so it's always the daemon's first line from us,
|
||||
* and a write failure here is harmless (the daemon just falls back to the
|
||||
* socket-close lifecycle). `hostPid` mirrors the PPID watchdog: the threaded
|
||||
* host pid if set, else our own parent (the host, on a no-relaunch bundle).
|
||||
*/
|
||||
function sendClientHello(socket: net.Socket): void {
|
||||
const clientHello: DaemonClientHello = {
|
||||
codegraph_client: 1,
|
||||
pid: process.pid,
|
||||
hostPid: parseHostPpid(process.env[HOST_PPID_ENV]) ?? process.ppid,
|
||||
};
|
||||
try { socket.write(JSON.stringify(clientHello) + '\n'); } catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
type JsonRpc = Record<string, unknown>;
|
||||
|
||||
/** Dependencies the local-handshake proxy needs, injected by MCPServer (which
|
||||
|
||||
Reference in New Issue
Block a user