diff --git a/apps/cli/src/cli-client.ts b/apps/cli/src/cli-client.ts index 7354add..7297232 100644 --- a/apps/cli/src/cli-client.ts +++ b/apps/cli/src/cli-client.ts @@ -31,24 +31,35 @@ function requestConnection(handshake: CliHandshake, timeoutMs: number): Promise< fn(); }; - sock.setEncoding("utf8"); - sock.setTimeout(timeoutMs, () => - settle(() => reject(new Error("Timed out waiting for the app to accept the connection"))), - ); - sock.on("connect", () => sock.end(JSON.stringify(handshake))); - sock.on("data", (chunk) => { - buf += chunk; - }); - sock.on("end", () => { + const settleReply = (raw: string) => { let reply: CliHandshakeReply; try { - reply = JSON.parse(buf) as CliHandshakeReply; + reply = JSON.parse(raw) as CliHandshakeReply; } catch (e) { settle(() => reject(e instanceof Error ? e : new Error(String(e)))); return; } if (reply.ok) settle(resolve); else settle(() => reject(new Error(reply.error))); + }; + + sock.setEncoding("utf8"); + sock.setTimeout(timeoutMs, () => + settle(() => reject(new Error("Timed out waiting for the app to accept the connection"))), + ); + // Windows named pipes do not support half-open: an `end()` here tears the + // pipe down before the app can reply. Frame the handshake with a newline + // instead and keep the socket open until the (newline-framed) reply lands. + sock.on("connect", () => sock.write(JSON.stringify(handshake) + "\n")); + sock.on("data", (chunk) => { + buf += chunk; + const nl = buf.indexOf("\n"); + if (nl !== -1) settleReply(buf.slice(0, nl)); + }); + sock.on("end", () => { + // Unix servers may still close without a trailing newline. + if (buf.length > 0) settleReply(buf); + else settle(() => reject(new Error("App closed the handshake socket without replying"))); }); sock.on("error", (err) => settle(() => reject(err))); }); diff --git a/apps/desktop/src/cli-server.ts b/apps/desktop/src/cli-server.ts index c78cf30..6290648 100644 --- a/apps/desktop/src/cli-server.ts +++ b/apps/desktop/src/cli-server.ts @@ -97,7 +97,7 @@ async function deliverHandshake(handshake: CliHandshake, sock: Socket): Promise< } catch (err) { reply = { ok: false, error: (err as Error).message }; } - if (!sock.destroyed) sock.end(JSON.stringify(reply)); + if (!sock.destroyed) sock.end(JSON.stringify(reply) + "\n"); } export function startCliServer() { @@ -107,24 +107,35 @@ export function startCliServer() { cliServer = createServer({ allowHalfOpen: true }, (sock: Socket) => { enableHeadless(); let buf = ""; - sock.setEncoding("utf8"); - sock.setTimeout(60000, () => sock.destroy()); - sock.on("data", (chunk) => { - buf += chunk; - }); - sock.on("end", async () => { + let handled = false; + const handle = async (raw: string) => { + if (handled) return; + handled = true; sock.setTimeout(0); let handshake: CliHandshake; try { - handshake = JSON.parse(buf) as CliHandshake; + handshake = JSON.parse(raw) as CliHandshake; if (typeof handshake.port !== "number" || typeof handshake.token !== "string") { throw new Error("Malformed handshake"); } } catch { - sock.end(JSON.stringify({ ok: false, error: "Invalid handshake" })); + sock.end(JSON.stringify({ ok: false, error: "Invalid handshake" }) + "\n"); return; } await deliverHandshake(handshake, sock); + }; + sock.setEncoding("utf8"); + sock.setTimeout(60000, () => sock.destroy()); + // Newline-framed clients (required on Windows, where named pipes cannot + // half-open) are answered as soon as the first line arrives; legacy + // clients that frame by half-closing are handled on "end". + sock.on("data", (chunk) => { + buf += chunk; + const nl = buf.indexOf("\n"); + if (nl !== -1) void handle(buf.slice(0, nl)); + }); + sock.on("end", () => { + void handle(buf); }); sock.on("error", () => { // Client hung up; nothing to do.