An MCP host that kills the launcher chain within the server's first ~100ms while keeping the stdio pipes open (config probe, cancelled request, startup timeout; Rust hosts that kill a child without dropping its stdio handles) left the server orphaned: it booted already reparented to init, so the PPID watchdog's "ppid changed" baseline was captured as 1 and could never fire, and stdin never EOF'd. The process lingered — idle, ~30MB — until the host itself exited, accumulating one per abandoned launch (the pile-up reported in #1185). Reproduced on released 1.2.0/macOS: SIGKILL the launcher at +50ms → permanent orphan; at +150ms the old late baseline had already run and reaped it. Three-part fix: - Capture process.ppid at the earliest line of the CLI entry (early-ppid.ts) and use it as every watchdog baseline, shrinking the blind window to the few ms before our first JS runs. - Thread the real host pid down the bundled path: the npm shim and the standalone sh launcher set CODEGRAPH_HOST_PPID (an outer launcher's value wins), so the watchdog polls the host directly. Previously only the --liftoff-only relaunch set it, leaving the entire npm/standalone install base with hostPpid=null. - Never-initialized backstop (startup-handshake.ts): a serve --mcp that receives no MCP traffic for CODEGRAPH_STARTUP_HANDSHAKE_TIMEOUT_MS (default 15min, 0 disables) shuts down — the catch-all for a kill landing in the residual pre-JS window. Disarmed on the first byte, so a quiet-but-live session is never touched. Also scrub CODEGRAPH_HOST_PPID from the detached daemon's env — it has no host, and a stale pid must not leak into anything it spawns. Validated end-to-end on the built bundle: the +50ms early-kill orphan is now reaped while the host still holds the pipes open, and all six normal lifecycle paths (clean close, SIGTERM/SIGKILL child, host exit/SIGKILL, fd-holding adversarial host) stay clean. New coverage in startup-handshake.test.ts, mcp-startup-orphan.test.ts, and npm-shim.test.ts. 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
6ea65246a5
commit
c9f8c0ebaf
@@ -57,6 +57,15 @@ function writeLauncher(binDir: string): void {
|
||||
fs.chmodSync(p, 0o755);
|
||||
}
|
||||
|
||||
// A fake bundle launcher that echoes the threaded host pid, so we can prove the
|
||||
// shim passed CODEGRAPH_HOST_PPID down to the server (#1185).
|
||||
function writeHostPpidLauncher(binDir: string): void {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const p = path.join(binDir, 'codegraph');
|
||||
fs.writeFileSync(p, '#!/bin/sh\necho "HOST_PPID=[${CODEGRAPH_HOST_PPID}]"\n');
|
||||
fs.chmodSync(p, 0o755);
|
||||
}
|
||||
|
||||
// Launch the shim with async spawn so the in-process HTTPS server can respond
|
||||
// while it runs (spawnSync would block this event loop and deadlock).
|
||||
function runShim(pkgDir: string, args: string[], env: Record<string, string>) {
|
||||
@@ -160,6 +169,39 @@ describe.skipIf(isWindows)('npm-shim launcher', () => {
|
||||
expect(r.stderr).toContain('--registry=https://registry.npmjs.org');
|
||||
expect(r.stderr).toContain('install.sh');
|
||||
});
|
||||
|
||||
// #1185: the shim threads the MCP host's pid (its own parent) down to the
|
||||
// bundled server so the server's orphan watchdog can poll the host directly
|
||||
// — the fix for a server left orphaned when the launcher is killed during its
|
||||
// startup. The shim's own parent here is the vitest runner (a real live pid).
|
||||
it('threads CODEGRAPH_HOST_PPID to the bundled server (#1185)', async () => {
|
||||
const pkg = makePkg();
|
||||
const platformPkg = path.join(pkg, 'node_modules', '@colbymchenry', `codegraph-${target}`);
|
||||
writeHostPpidLauncher(path.join(platformPkg, 'bin'));
|
||||
fs.writeFileSync(path.join(platformPkg, 'package.json'),
|
||||
JSON.stringify({ name: `@colbymchenry/codegraph-${target}`, version: '9.9.9-test' }) + '\n');
|
||||
const r = await runShim(pkg, [], { CODEGRAPH_INSTALL_DIR: mkTmp('cache') });
|
||||
|
||||
expect(r.status).toBe(0);
|
||||
// Non-empty and numeric — the shim's parent pid was passed through.
|
||||
const m = r.stdout.match(/HOST_PPID=\[(\d+)\]/);
|
||||
expect(m, `expected a numeric HOST_PPID, got: ${r.stdout}`).not.toBeNull();
|
||||
expect(Number(m![1])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('does not clobber an already-set CODEGRAPH_HOST_PPID (#1185)', async () => {
|
||||
const pkg = makePkg();
|
||||
const platformPkg = path.join(pkg, 'node_modules', '@colbymchenry', `codegraph-${target}`);
|
||||
writeHostPpidLauncher(path.join(platformPkg, 'bin'));
|
||||
fs.writeFileSync(path.join(platformPkg, 'package.json'),
|
||||
JSON.stringify({ name: `@colbymchenry/codegraph-${target}`, version: '9.9.9-test' }) + '\n');
|
||||
// An outer launcher already threaded the true host pid — it must win over
|
||||
// the shim's own parent, or a chain of launchers would each overwrite it.
|
||||
const r = await runShim(pkg, [], { CODEGRAPH_INSTALL_DIR: mkTmp('cache'), CODEGRAPH_HOST_PPID: '424242' });
|
||||
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain('HOST_PPID=[424242]');
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!CAN_NET)('npm-shim download fallback (local HTTPS)', () => {
|
||||
|
||||
Reference in New Issue
Block a user