* Read the optional client-hello line a proxy sends after the daemon hello. * Always resolves (never rejects) — fail-safe by design, since every connection * funnels through here. Resolves with the peer pids when the first line is a * client-hello; otherwise resolves with null pids and unshifts t
( socket: net.Socket, )
| 791 | * straddling a chunk boundary in the unshifted tail is never corrupted. |
| 792 | */ |
| 793 | function readClientHello( |
| 794 | socket: net.Socket, |
| 795 | ): Promise<{ pid: number | null; hostPid: number | null }> { |
| 796 | return new Promise((resolve) => { |
| 797 | let chunks: Buffer[] = []; |
| 798 | let total = 0; |
| 799 | let settled = false; |
| 800 | const finish = ( |
| 801 | peers: { pid: number | null; hostPid: number | null }, |
| 802 | putBack?: Buffer, |
| 803 | ) => { |
| 804 | if (settled) return; |
| 805 | settled = true; |
| 806 | // PAUSE before detaching: removing the last 'data' listener does NOT |
| 807 | // stop a flowing stream, so bytes arriving (or unshifted) in the gap |
| 808 | // between this handler and the session transport attaching were emitted |
| 809 | // to zero listeners and silently DISCARDED — and the listener swap left |
| 810 | // the socket's flow state wedged, never delivering to the new listener. |
| 811 | // A proxy whose client-hello arrived glued to the initialize hit this |
| 812 | // ~1-in-5 under load: the daemon answered nothing for the whole session |
| 813 | // (the #662 test flake, and real dead sessions behind it). Paused, the |
| 814 | // unshifted tail and any new bytes buffer; SocketTransport.start() |
| 815 | // resumes explicitly. |
| 816 | try { socket.pause(); } catch { /* stream already gone */ } |
| 817 | socket.removeListener('data', onData); |
| 818 | socket.removeListener('error', onEnd); |
| 819 | socket.removeListener('close', onEnd); |
| 820 | clearTimeout(timer); |
| 821 | if (process.env.CODEGRAPH_MCP_DEBUG) { |
| 822 | process.stderr.write(`[mcp-debug] clientHello finish pid=${String(peers.pid)} putBack=${putBack ? putBack.length : 0} flowing=${String(socket.readableFlowing)}\n`); |
| 823 | } |
| 824 | if (putBack && putBack.length > 0 && !socket.destroyed) { |
| 825 | try { socket.unshift(putBack); } catch { /* stream already gone */ } |
| 826 | } |
| 827 | resolve(peers); |
| 828 | }; |
| 829 | const onData = (chunk: Buffer | string) => { |
| 830 | const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk; |
| 831 | chunks.push(buf); |
| 832 | total += buf.length; |
| 833 | const all = chunks.length === 1 ? buf : Buffer.concat(chunks, total); |
| 834 | const nl = all.indexOf(0x0a); // '\n' |
| 835 | if (nl === -1) { |
| 836 | // No newline yet. If it's already too long to be a hello, it isn't one — |
| 837 | // hand the bytes back as data; otherwise keep accumulating. |
| 838 | if (total > MAX_HELLO_LINE_BYTES) finish({ pid: null, hostPid: null }, all); |
| 839 | else chunks = [all]; |
| 840 | return; |
| 841 | } |
| 842 | const peers = parseClientHelloLine(all.subarray(0, nl).toString('utf8')); |
| 843 | if (peers) { |
| 844 | const tail = all.subarray(nl + 1); |
| 845 | finish(peers, tail.length > 0 ? tail : undefined); |
| 846 | } else { |
| 847 | // First line is not a client-hello (legacy/direct client) — hand the |
| 848 | // whole buffer back so the transport sees the message verbatim. |
| 849 | finish({ pid: null, hostPid: null }, all); |
| 850 | } |
no test coverage detected