diff --git a/src/node/cli.ts b/src/node/cli.ts index 926f6ff1fdef..623268f47e32 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -1,4 +1,5 @@ import { field, Level, logger } from "@coder/logger" +import * as crypto from "crypto" import { promises as fs } from "fs" import { load } from "js-yaml" import * as path from "path" @@ -561,7 +562,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config } if (!args["session-socket"]) { - args["session-socket"] = path.join(args["user-data-dir"], "code-server-ipc.sock") + args["session-socket"] = defaultSessionSocket(args["user-data-dir"]) } process.env.CODE_SERVER_SESSION_SOCKET = args["session-socket"] @@ -710,6 +711,25 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } +/** + * The session socket to use when one was not given. + * + * Windows has no Unix sockets, so there it is a named pipe, which lives in its + * own namespace rather than on disk and so cannot be placed inside the user + * data directory. The name is derived from that directory anyway, so that two + * instances with separate data directories do not collide and a later + * invocation with the same one finds the first. Windows paths are compared + * without regard to case, so the name is folded before it is hashed; otherwise + * the same directory typed two ways would produce two pipes. + */ +export function defaultSessionSocket(userDataDir: string, platform: NodeJS.Platform = process.platform): string { + if (platform !== "win32") { + return path.join(userDataDir, "code-server-ipc.sock") + } + const name = crypto.createHash("sha256").update(path.resolve(userDataDir).toLowerCase()).digest("hex").slice(0, 16) + return String.raw`\\.\pipe\code-server-ipc-${name}` +} + export function getResolvedPathsFromArgs(args: UserProvidedArgs): string[] { return (args._ ?? []).map((p) => path.resolve(p)) } diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index 53eb066c1e70..17f57a9666ef 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -5,6 +5,7 @@ import { UserProvidedArgs, bindAddrFromArgs, defaultConfigFile, + defaultSessionSocket, parse, parseConfigFile, setDefaults, @@ -37,7 +38,7 @@ const defaults = { usingEnvHashedPassword: false, "extensions-dir": path.join(paths.data, "extensions"), "user-data-dir": paths.data, - "session-socket": path.join(paths.data, "code-server-ipc.sock"), + "session-socket": defaultSessionSocket(paths.data), "app-name": "code-server", _: [], } @@ -977,6 +978,26 @@ describe("bindAddrFromArgs", () => { }) }) +describe("defaultSessionSocket", () => { + const dataDir = path.join("/home/coder/.local/share", "code-server") + + it("should put the socket in the user data directory", () => { + expect(defaultSessionSocket(dataDir, "linux")).toBe(path.join(dataDir, "code-server-ipc.sock")) + }) + + it("should use a named pipe on windows", () => { + expect(defaultSessionSocket(dataDir, "win32")).toMatch(/^\\\\\.\\pipe\\code-server-ipc-[0-9a-f]{16}$/) + }) + + it("should give separate data directories separate pipes", () => { + expect(defaultSessionSocket(dataDir, "win32")).not.toBe(defaultSessionSocket(dataDir + "-other", "win32")) + }) + + it("should give one data directory one pipe however it is spelled", () => { + expect(defaultSessionSocket(dataDir.toUpperCase(), "win32")).toBe(defaultSessionSocket(dataDir, "win32")) + }) +}) + describe("defaultConfigFile", () => { it("should return the default config file as a string", async () => { const password = await generatePassword()