* Pipe stdin → socket and socket → stdout. Resolves once either end closes * so the process can exit. Note: we deliberately do NOT use * `process.stdin.pipe(socket)` because pipe propagates 'end' onto the * downstream, which would close the socket prematurely if stdin happens to * end early — th
(socket: net.Socket)
| 478 | * end early — the MCP spec allows it to stay open across reconnects. |
| 479 | */ |
| 480 | function pipeUntilClose(socket: net.Socket): Promise<void> { |
| 481 | return new Promise((resolve) => { |
| 482 | let resolved = false; |
| 483 | const done = () => { if (!resolved) { resolved = true; resolve(); } }; |
| 484 | |
| 485 | process.stdin.on('data', (chunk) => { |
| 486 | try { socket.write(chunk); } catch { /* socket may have errored — close path catches it */ } |
| 487 | }); |
| 488 | process.stdin.on('end', () => { |
| 489 | try { socket.end(); } catch { /* ignore */ } |
| 490 | done(); |
| 491 | }); |
| 492 | // 'close' and 'error' both tear down: a socket-backed stdin can fail with |
| 493 | // an 'error' (ECONNRESET/hangup) rather than a clean close; destroying it |
| 494 | // stops a hung fd from busy-spinning the event loop (#799). |
| 495 | const teardown = () => { |
| 496 | try { process.stdin.destroy(); } catch { /* ignore */ } |
| 497 | try { socket.destroy(); } catch { /* ignore */ } |
| 498 | done(); |
| 499 | }; |
| 500 | process.stdin.on('close', teardown); |
| 501 | process.stdin.on('error', teardown); |
| 502 | |
| 503 | socket.on('data', (chunk) => { |
| 504 | try { process.stdout.write(chunk); } catch { /* ignore */ } |
| 505 | }); |
| 506 | socket.on('end', () => done()); |
| 507 | socket.on('close', () => done()); |
| 508 | socket.on('error', (err) => { |
| 509 | process.stderr.write(`[CodeGraph MCP] daemon socket error: ${err.message}\n`); |
| 510 | done(); |
| 511 | }); |
| 512 | }); |
| 513 | } |
| 514 | |
| 515 | /** |
| 516 | * PPID watchdog mirroring the one in `MCPServer.start` — kills the proxy if |