Skip to content

Commit 8633448

Browse files
committed
Default the session socket to a named pipe on windows
The session socket defaults to a path inside the user data directory, which windows cannot bind: it has no Unix sockets. The bind failure is caught and warned about, and the server is returned anyway, so startup prints warn Could not create socket at ...\code-server-ipc.sock info Session server listening on ...\code-server-ipc.sock about a socket that does not exist, and nothing listens on it. That socket is what shouldOpenInExistingInstance consults, so on windows `code-server <file>` run from a shell can never find a running instance and starts its own server instead. Windows has the same facility under a different name, so this defaults to a named pipe there. The name is derived from the user data directory, so instances with separate data directories do not collide and a later invocation with the same one finds the first, and it is case-folded first because windows compares paths that way and the same directory typed two ways should not produce two pipes. An explicitly passed --session-socket is untouched on every platform. Nothing downstream needed changing: the paths are only ever handed to net.connect and http.request, both of which take a pipe name on windows, and listen()'s unlink-first only has to tolerate ENOENT, which it already does. Measured against a real windows build, one flag apart: --session-socket '\.\pipe\...' no warning; "Session server listening on" that pipe; canConnect true; GET /session answers 200 (default) the warning above, the phantom "listening" line, and no such file on disk The helper takes the platform rather than reading process.platform directly, following getEnvPaths and constructOpenOptions, so both branches are tested where the tests run.
1 parent 62284ed commit 8633448

2 files changed

Lines changed: 43 additions & 2 deletions

File tree

‎src/node/cli.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { field, Level, logger } from "@coder/logger"
2+
import * as crypto from "crypto"
23
import { promises as fs } from "fs"
34
import { load } from "js-yaml"
45
import * as path from "path"
@@ -561,7 +562,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
561562
}
562563

563564
if (!args["session-socket"]) {
564-
args["session-socket"] = path.join(args["user-data-dir"], "code-server-ipc.sock")
565+
args["session-socket"] = defaultSessionSocket(args["user-data-dir"])
565566
}
566567
process.env.CODE_SERVER_SESSION_SOCKET = args["session-socket"]
567568

@@ -710,6 +711,25 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
710711
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
711712
}
712713

714+
/**
715+
* The session socket to use when one was not given.
716+
*
717+
* Windows has no Unix sockets, so there it is a named pipe, which lives in its
718+
* own namespace rather than on disk and so cannot be placed inside the user
719+
* data directory. The name is derived from that directory anyway, so that two
720+
* instances with separate data directories do not collide and a later
721+
* invocation with the same one finds the first. Windows paths are compared
722+
* without regard to case, so the name is folded before it is hashed; otherwise
723+
* the same directory typed two ways would produce two pipes.
724+
*/
725+
export function defaultSessionSocket(userDataDir: string, platform: NodeJS.Platform = process.platform): string {
726+
if (platform !== "win32") {
727+
return path.join(userDataDir, "code-server-ipc.sock")
728+
}
729+
const name = crypto.createHash("sha256").update(path.resolve(userDataDir).toLowerCase()).digest("hex").slice(0, 16)
730+
return String.raw`\\.\pipe\code-server-ipc-${name}`
731+
}
732+
713733
export function getResolvedPathsFromArgs(args: UserProvidedArgs): string[] {
714734
return (args._ ?? []).map((p) => path.resolve(p))
715735
}

‎test/unit/node/cli.test.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
UserProvidedArgs,
66
bindAddrFromArgs,
77
defaultConfigFile,
8+
defaultSessionSocket,
89
parse,
910
parseConfigFile,
1011
setDefaults,
@@ -37,7 +38,7 @@ const defaults = {
3738
usingEnvHashedPassword: false,
3839
"extensions-dir": path.join(paths.data, "extensions"),
3940
"user-data-dir": paths.data,
40-
"session-socket": path.join(paths.data, "code-server-ipc.sock"),
41+
"session-socket": defaultSessionSocket(paths.data),
4142
"app-name": "code-server",
4243
_: [],
4344
}
@@ -977,6 +978,26 @@ describe("bindAddrFromArgs", () => {
977978
})
978979
})
979980

981+
describe("defaultSessionSocket", () => {
982+
const dataDir = path.join("/home/coder/.local/share", "code-server")
983+
984+
it("should put the socket in the user data directory", () => {
985+
expect(defaultSessionSocket(dataDir, "linux")).toBe(path.join(dataDir, "code-server-ipc.sock"))
986+
})
987+
988+
it("should use a named pipe on windows", () => {
989+
expect(defaultSessionSocket(dataDir, "win32")).toMatch(/^\\\\\.\\pipe\\code-server-ipc-[0-9a-f]{16}$/)
990+
})
991+
992+
it("should give separate data directories separate pipes", () => {
993+
expect(defaultSessionSocket(dataDir, "win32")).not.toBe(defaultSessionSocket(dataDir + "-other", "win32"))
994+
})
995+
996+
it("should give one data directory one pipe however it is spelled", () => {
997+
expect(defaultSessionSocket(dataDir.toUpperCase(), "win32")).toBe(defaultSessionSocket(dataDir, "win32"))
998+
})
999+
})
1000+
9801001
describe("defaultConfigFile", () => {
9811002
it("should return the default config file as a string", async () => {
9821003
const password = await generatePassword()

0 commit comments

Comments
 (0)