From 45e6d072aa8c6a77b3083c0e3793b470227e9684 Mon Sep 17 00:00:00 2001 From: Clay Tercek <30105080+claytercek@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:59:09 -0700 Subject: [PATCH 1/3] fix(controller): fix windows named pipe path --- .changeset/gold-worlds-switch.md | 5 ++ package-lock.json | 13 +++++ packages/controller/package.json | 1 + packages/controller/src/ipc/ipc-client.ts | 5 +- packages/controller/src/ipc/ipc-utils.ts | 21 +++++++++ .../src/transports/ipc-transport.ts | 47 +++++++++++++------ 6 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 .changeset/gold-worlds-switch.md create mode 100644 packages/controller/src/ipc/ipc-utils.ts diff --git a/.changeset/gold-worlds-switch.md b/.changeset/gold-worlds-switch.md new file mode 100644 index 00000000..7275c63b --- /dev/null +++ b/.changeset/gold-worlds-switch.md @@ -0,0 +1,5 @@ +--- +"@bluecadet/launchpad-controller": patch +--- + +Fix invalid named pipe on windows diff --git a/package-lock.json b/package-lock.json index eb72d268..13ef113c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10384,6 +10384,7 @@ "license": "MIT", "dependencies": { "@bluecadet/launchpad-utils": "~2.1.0", + "chalk": "^5.0.0", "devalue": "^5.4.2", "immer": "^10.2.0", "neverthrow": "^8.1.1", @@ -10398,6 +10399,18 @@ "node": ">=18" } }, + "packages/controller/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "packages/dashboard": { "name": "@bluecadet/launchpad-dashboard", "version": "2.0.0", diff --git a/packages/controller/package.json b/packages/controller/package.json index 6648b9fe..9f73e912 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -37,6 +37,7 @@ "homepage": "https://github.com/bluecadet/launchpad/packages/controller", "dependencies": { "@bluecadet/launchpad-utils": "~2.1.0", + "chalk": "^5.0.0", "devalue": "^5.4.2", "immer": "^10.2.0", "neverthrow": "^8.1.1", diff --git a/packages/controller/src/ipc/ipc-client.ts b/packages/controller/src/ipc/ipc-client.ts index 664f5ce3..1a41bcea 100644 --- a/packages/controller/src/ipc/ipc-client.ts +++ b/packages/controller/src/ipc/ipc-client.ts @@ -12,6 +12,7 @@ import type { LaunchpadState } from "../core/state-store.js"; import { IPCConnectionError, IPCMessageError, IPCTimeoutError } from "../errors.js"; import type { IPCBroadcastMessage, IPCMessage, IPCResponse } from "../transports/ipc-transport.js"; import { IPCSerializer } from "./ipc-serializer.js"; +import { getOSSocketPath } from "./ipc-utils.js"; enablePatches(); @@ -37,7 +38,9 @@ export class IPCClient { /** * Connect to the IPC socket */ - connect(socketPath: string): ResultAsync { + connect(originalSocketPath: string): ResultAsync { + const socketPath = getOSSocketPath(originalSocketPath); + return ResultAsync.fromPromise( new Promise((resolve, reject) => { this._socket = net.createConnection(socketPath, () => { diff --git a/packages/controller/src/ipc/ipc-utils.ts b/packages/controller/src/ipc/ipc-utils.ts new file mode 100644 index 00000000..e03b5e29 --- /dev/null +++ b/packages/controller/src/ipc/ipc-utils.ts @@ -0,0 +1,21 @@ +import path from "node:path"; + +/** + * Ensure path conforms with OS requirements (only relevant for windows ATM) + * https://nodejs.org/api/net.html#identifying-paths-for-ipc-connections + */ +export function getOSSocketPath(socketPath: string) { + if (process.platform === "win32") { + // On Windows, we need to use a named pipe + + if (socketPath.includes("\\\\?\\pipe") || socketPath.includes("\\\\.\\pipe")) { + // Already in correct format + return socketPath; + } + return path.join( + '\\\\?\\pipe', + socketPath + ) + } + return socketPath; +} \ No newline at end of file diff --git a/packages/controller/src/transports/ipc-transport.ts b/packages/controller/src/transports/ipc-transport.ts index 8288ba8e..11417a48 100644 --- a/packages/controller/src/transports/ipc-transport.ts +++ b/packages/controller/src/transports/ipc-transport.ts @@ -11,6 +11,7 @@ import type { Patch } from "immer"; import { ResultAsync } from "neverthrow"; import type { VersionedLaunchpadState } from "../core/state-store.js"; import type { Transport, TransportContext } from "../core/transport.js"; +import chalk from 'chalk' import { CommandExecutionError, IPCMessageError, @@ -18,6 +19,7 @@ import { TransportError, } from "../errors.js"; import { IPCSerializer } from "../ipc/ipc-serializer.js"; +import { getOSSocketPath } from "../ipc/ipc-utils.js"; export type IPCTransportOptions = { /** Path to the Unix socket file */ @@ -54,12 +56,25 @@ export function createIPCTransport(options: IPCTransportOptions): Transport { let server: net.Server | null = null; const clients = new Set(); + const socketPath = getOSSocketPath(options.socketPath); + return { id: "ipc", start(ctx: TransportContext): ResultAsync { const { logger, abortSignal } = ctx; - const { socketPath } = options; + + if (process.platform == "win32" && (options.socketPath !== socketPath)) { + // notify user that the socket path has been updated to conform with windows named pipe reqs, + // as it might not be where they expect it + + logger.warn( + `Windows named pipes must be located in ${chalk.grey("\\\\?\\pipe\\")} or ${chalk.grey("\\\\.\\pipe\\")}. ` + ) + logger.warn( + `The configured socketPath has been moved to the ${chalk.grey("\\\\?\\pipe\\")} directory to conform with this requirement.` + ) + } const promise = new Promise((resolve, reject) => { // Clean up existing socket @@ -75,18 +90,21 @@ export function createIPCTransport(options: IPCTransportOptions): Transport { } } - // Ensure directory exists - try { - const dir = path.dirname(socketPath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + // This step isn't relevant for windows named pipes + if (process.platform !== "win32") { + // Ensure directory exists + try { + const dir = path.dirname(socketPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } catch (e) { + return reject( + new TransportError("Failed to create socket directory", { + cause: e instanceof Error ? e : new Error(String(e)), + }), + ); } - } catch (e) { - return reject( - new TransportError("Failed to create socket directory", { - cause: e instanceof Error ? e : new Error(String(e)), - }), - ); } // Create server @@ -139,7 +157,7 @@ export function createIPCTransport(options: IPCTransportOptions): Transport { }); server.listen(socketPath, () => { - logger.info(`IPC transport listening at ${socketPath}`); + logger.info(`IPC transport listening at ${chalk.grey(socketPath)}`); // Subscribe to EventBus for event streaming with type-safe handler const handleEvent = ( @@ -208,7 +226,6 @@ export function createIPCTransport(options: IPCTransportOptions): Transport { stop(ctx: TransportContext): ResultAsync { const { logger } = ctx; - const { socketPath } = options; const promise = new Promise((resolve, reject) => { if (!server) { @@ -368,4 +385,4 @@ function sendError(socket: net.Socket, id: string, error: Error): void { type: "error", error: error, }); -} +} \ No newline at end of file From 9a48fb89dbc08876439a909b86cd681e04e0eeea Mon Sep 17 00:00:00 2001 From: Clay Tercek <30105080+claytercek@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:01:24 -0700 Subject: [PATCH 2/3] test(controller): fix tests for ipc path correction on windows --- .../src/ipc/__tests__/ipc-client.test.ts | 3 ++- .../src/ipc/__tests__/ipc-utils.test.ts | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 packages/controller/src/ipc/__tests__/ipc-utils.test.ts diff --git a/packages/controller/src/ipc/__tests__/ipc-client.test.ts b/packages/controller/src/ipc/__tests__/ipc-client.test.ts index 6d35b516..0c765290 100644 --- a/packages/controller/src/ipc/__tests__/ipc-client.test.ts +++ b/packages/controller/src/ipc/__tests__/ipc-client.test.ts @@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { IPCEvent, IPCResponse } from "../../transports/ipc-transport.js"; import { IPCClient } from "../ipc-client.js"; import { IPCSerializer } from "../ipc-serializer.js"; +import { getOSSocketPath } from "../ipc-utils.js"; type Cb = (...args: any[]) => void; @@ -84,7 +85,7 @@ describe("IPCClient", () => { const result = await client.connect("/test/socket"); expect(result.isOk()).toBe(true); - expect(net.createConnection).toHaveBeenCalledWith("/test/socket", expect.any(Function)); + expect(net.createConnection).toHaveBeenCalledWith(getOSSocketPath("/test/socket"), expect.any(Function)); }); it("should return error if connection fails", async () => { diff --git a/packages/controller/src/ipc/__tests__/ipc-utils.test.ts b/packages/controller/src/ipc/__tests__/ipc-utils.test.ts new file mode 100644 index 00000000..be99d003 --- /dev/null +++ b/packages/controller/src/ipc/__tests__/ipc-utils.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { getOSSocketPath } from "../ipc-utils.js"; + +describe("getOSSocketPath", () => { + describe.runIf(process.platform !== "win32")("on posix", () => { + it("returns the same path", () => { + const inputPath = "/tmp/socket.sock"; + const result = getOSSocketPath(inputPath); + expect(result).toBe(inputPath); + }); + }); + + describe.runIf(process.platform === "win32")("on Windows", () => { + it('updates the path to use named pipe format if not already in that format', () => { + const inputPath = "C:\\temp\\socket.sock"; + const expectedPath = "\\\\?\\pipe\\C:\\temp\\socket.sock"; + const result = getOSSocketPath(inputPath); + expect(result).toBe(expectedPath); + }); + + it('returns the same path if already in named pipe format with \\\\?\\pipe prefix', () => { + const inputPath = "\\\\?\\pipe\\my_named_pipe"; + const result = getOSSocketPath(inputPath); + expect(result).toBe(inputPath); + }); + }); +}) \ No newline at end of file From 3f8d1d79e550ce5e3a614da1e048565550c0b30a Mon Sep 17 00:00:00 2001 From: Clay Tercek <30105080+claytercek@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:03:37 -0700 Subject: [PATCH 3/3] chore(controller): fix lint errors --- .../src/ipc/__tests__/ipc-client.test.ts | 5 ++- .../src/ipc/__tests__/ipc-utils.test.ts | 42 +++++++++---------- packages/controller/src/ipc/ipc-client.ts | 2 +- packages/controller/src/ipc/ipc-utils.ts | 23 +++++----- .../src/transports/ipc-transport.ts | 14 +++---- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/controller/src/ipc/__tests__/ipc-client.test.ts b/packages/controller/src/ipc/__tests__/ipc-client.test.ts index 0c765290..aacf87a5 100644 --- a/packages/controller/src/ipc/__tests__/ipc-client.test.ts +++ b/packages/controller/src/ipc/__tests__/ipc-client.test.ts @@ -85,7 +85,10 @@ describe("IPCClient", () => { const result = await client.connect("/test/socket"); expect(result.isOk()).toBe(true); - expect(net.createConnection).toHaveBeenCalledWith(getOSSocketPath("/test/socket"), expect.any(Function)); + expect(net.createConnection).toHaveBeenCalledWith( + getOSSocketPath("/test/socket"), + expect.any(Function), + ); }); it("should return error if connection fails", async () => { diff --git a/packages/controller/src/ipc/__tests__/ipc-utils.test.ts b/packages/controller/src/ipc/__tests__/ipc-utils.test.ts index be99d003..d91fecde 100644 --- a/packages/controller/src/ipc/__tests__/ipc-utils.test.ts +++ b/packages/controller/src/ipc/__tests__/ipc-utils.test.ts @@ -2,26 +2,26 @@ import { describe, expect, it } from "vitest"; import { getOSSocketPath } from "../ipc-utils.js"; describe("getOSSocketPath", () => { - describe.runIf(process.platform !== "win32")("on posix", () => { - it("returns the same path", () => { - const inputPath = "/tmp/socket.sock"; - const result = getOSSocketPath(inputPath); - expect(result).toBe(inputPath); - }); - }); + describe.runIf(process.platform !== "win32")("on posix", () => { + it("returns the same path", () => { + const inputPath = "/tmp/socket.sock"; + const result = getOSSocketPath(inputPath); + expect(result).toBe(inputPath); + }); + }); - describe.runIf(process.platform === "win32")("on Windows", () => { - it('updates the path to use named pipe format if not already in that format', () => { - const inputPath = "C:\\temp\\socket.sock"; - const expectedPath = "\\\\?\\pipe\\C:\\temp\\socket.sock"; - const result = getOSSocketPath(inputPath); - expect(result).toBe(expectedPath); - }); + describe.runIf(process.platform === "win32")("on Windows", () => { + it("updates the path to use named pipe format if not already in that format", () => { + const inputPath = "C:\\temp\\socket.sock"; + const expectedPath = "\\\\?\\pipe\\C:\\temp\\socket.sock"; + const result = getOSSocketPath(inputPath); + expect(result).toBe(expectedPath); + }); - it('returns the same path if already in named pipe format with \\\\?\\pipe prefix', () => { - const inputPath = "\\\\?\\pipe\\my_named_pipe"; - const result = getOSSocketPath(inputPath); - expect(result).toBe(inputPath); - }); - }); -}) \ No newline at end of file + it("returns the same path if already in named pipe format with \\\\?\\pipe prefix", () => { + const inputPath = "\\\\?\\pipe\\my_named_pipe"; + const result = getOSSocketPath(inputPath); + expect(result).toBe(inputPath); + }); + }); +}); diff --git a/packages/controller/src/ipc/ipc-client.ts b/packages/controller/src/ipc/ipc-client.ts index 1a41bcea..eacd69d8 100644 --- a/packages/controller/src/ipc/ipc-client.ts +++ b/packages/controller/src/ipc/ipc-client.ts @@ -232,7 +232,7 @@ export class IPCClient { try { this._lastState = applyPatches(this._lastState, patches); this._lastStateVersion = version; - } catch (e) { + } catch (_e) { return errAsync(new IPCMessageError("Failed to apply patches")); } diff --git a/packages/controller/src/ipc/ipc-utils.ts b/packages/controller/src/ipc/ipc-utils.ts index e03b5e29..21c72c83 100644 --- a/packages/controller/src/ipc/ipc-utils.ts +++ b/packages/controller/src/ipc/ipc-utils.ts @@ -5,17 +5,14 @@ import path from "node:path"; * https://nodejs.org/api/net.html#identifying-paths-for-ipc-connections */ export function getOSSocketPath(socketPath: string) { - if (process.platform === "win32") { - // On Windows, we need to use a named pipe + if (process.platform === "win32") { + // On Windows, we need to use a named pipe - if (socketPath.includes("\\\\?\\pipe") || socketPath.includes("\\\\.\\pipe")) { - // Already in correct format - return socketPath; - } - return path.join( - '\\\\?\\pipe', - socketPath - ) - } - return socketPath; -} \ No newline at end of file + if (socketPath.includes("\\\\?\\pipe") || socketPath.includes("\\\\.\\pipe")) { + // Already in correct format + return socketPath; + } + return path.join("\\\\?\\pipe", socketPath); + } + return socketPath; +} diff --git a/packages/controller/src/transports/ipc-transport.ts b/packages/controller/src/transports/ipc-transport.ts index 11417a48..30fbbfd1 100644 --- a/packages/controller/src/transports/ipc-transport.ts +++ b/packages/controller/src/transports/ipc-transport.ts @@ -7,11 +7,11 @@ import fs from "node:fs"; import net from "node:net"; import path from "node:path"; import type { LaunchpadEvents } from "@bluecadet/launchpad-utils"; +import chalk from "chalk"; import type { Patch } from "immer"; import { ResultAsync } from "neverthrow"; import type { VersionedLaunchpadState } from "../core/state-store.js"; import type { Transport, TransportContext } from "../core/transport.js"; -import chalk from 'chalk' import { CommandExecutionError, IPCMessageError, @@ -64,16 +64,16 @@ export function createIPCTransport(options: IPCTransportOptions): Transport { start(ctx: TransportContext): ResultAsync { const { logger, abortSignal } = ctx; - if (process.platform == "win32" && (options.socketPath !== socketPath)) { + if (process.platform === "win32" && options.socketPath !== socketPath) { // notify user that the socket path has been updated to conform with windows named pipe reqs, // as it might not be where they expect it logger.warn( - `Windows named pipes must be located in ${chalk.grey("\\\\?\\pipe\\")} or ${chalk.grey("\\\\.\\pipe\\")}. ` - ) + `Windows named pipes must be located in ${chalk.grey("\\\\?\\pipe\\")} or ${chalk.grey("\\\\.\\pipe\\")}. `, + ); logger.warn( - `The configured socketPath has been moved to the ${chalk.grey("\\\\?\\pipe\\")} directory to conform with this requirement.` - ) + `The configured socketPath has been moved to the ${chalk.grey("\\\\?\\pipe\\")} directory to conform with this requirement.`, + ); } const promise = new Promise((resolve, reject) => { @@ -385,4 +385,4 @@ function sendError(socket: net.Socket, id: string, error: Error): void { type: "error", error: error, }); -} \ No newline at end of file +}