From f59cfb9392277997bf035a6cd4cda80d9c957d9f Mon Sep 17 00:00:00 2001 From: freetag369 <268631876+freetag369@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:09:14 +0900 Subject: [PATCH] Fix CLI handshake on Windows named pipes with newline framing The CLI handshake framed its messages by half-closing the socket: the client wrote the handshake and called sock.end(), then parsed the reply on 'end'. Windows named pipes (\\.\pipe\diffusion-studio) do not support half-open connections, so end() tears the whole pipe down before the app can reply, the client reads an empty buffer, and every dapi command that connects to the app fails with "Unexpected end of JSON input". Frame the handshake with a newline instead: the client writes the JSON followed by "\n" and keeps the socket open; the server answers as soon as the first line arrives and terminates its reply with "\n". Peers that still frame by half-closing are handled on 'end' on both sides, so new clients and servers stay compatible with old ones on Unix sockets. Repro (Windows 11, Node 22): launch the desktop app, then run any dapi command that talks to it (e.g. `dapi mount `). Before this change the handshake always fails; after it, it succeeds. Co-Authored-By: Claude Fable 5 --- apps/cli/src/cli-client.ts | 31 +++++++++++++++++++++---------- apps/desktop/src/cli-server.ts | 29 ++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) 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.