* 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)
| 507 | * end early — the MCP spec allows it to stay open across reconnects. |
| 508 | */ |
| 509 | function pipeUntilClose(socket: net.Socket): Promise<void> { |
| 510 | return new Promise((resolve) => { |
| 511 | let resolved = false; |
| 512 | const done = () => { if (!resolved) { resolved = true; resolve(); } }; |
| 513 | |
| 514 | process.stdin.on('data', (chunk) => { |
| 515 | try { socket.write(chunk); } catch { /* socket may have errored — close path catches it */ } |
| 516 | }); |
| 517 | process.stdin.on('end', () => { |
| 518 | try { socket.end(); } catch { /* ignore */ } |
| 519 | done(); |
| 520 | }); |
| 521 | // 'close' and 'error' both tear down: a socket-backed stdin can fail with |
| 522 | // an 'error' (ECONNRESET/hangup) rather than a clean close; destroying it |
| 523 | // stops a hung fd from busy-spinning the event loop (#799). |
| 524 | const teardown = () => { |
| 525 | try { process.stdin.destroy(); } catch { /* ignore */ } |
| 526 | try { socket.destroy(); } catch { /* ignore */ } |
| 527 | done(); |
| 528 | }; |
| 529 | process.stdin.on('close', teardown); |
| 530 | process.stdin.on('error', teardown); |
| 531 | |
| 532 | socket.on('data', (chunk) => { |
| 533 | try { process.stdout.write(chunk); } catch { /* ignore */ } |
| 534 | }); |
| 535 | socket.on('end', () => done()); |
| 536 | socket.on('close', () => done()); |
| 537 | socket.on('error', (err) => { |
| 538 | process.stderr.write(`[CodeGraph MCP] daemon socket error: ${err.message}\n`); |
| 539 | done(); |
| 540 | }); |
| 541 | }); |
| 542 | } |
| 543 | |
| 544 | /** |
| 545 | * PPID watchdog mirroring the one in `MCPServer.start` — kills the proxy if |