mirror of
https://github.com/coder/code-server.git
synced 2026-11-02 22:09:11 +01:00
Compare commits
2 Commits
761b0d2a19
...
27808460b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27808460b1 | ||
|
|
70fc911358 |
@@ -22,6 +22,12 @@ Code v99.99.999
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- `--socket-fd` now works: the inherited socket is passed on to the process
|
||||
that listens, and the address is logged as `fd:<n>`. Previously code-server
|
||||
failed with `listen EEXIST` or `Server has no address`.
|
||||
|
||||
## [4.143.1](https://github.com/coder/code-server/releases/tag/v4.143.1) - 2026-10-23
|
||||
|
||||
Code v1.143.1
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1726,9 +1726,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
|
||||
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.3.tgz",
|
||||
"integrity": "sha512-T7GYtQ2eKJUN5YORG7A8u2WfB9JMN0a4/wl3MI64qq2qTRfa3FXzoNq0DDojZDkDyStcL+TkfKsz4JHEZx5kGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"resolutions": {
|
||||
"@types/node": "24.x"
|
||||
},
|
||||
"overrides": {
|
||||
"basic-ftp": "^6.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"code-server": "out/node/entry.js"
|
||||
},
|
||||
|
||||
@@ -111,9 +111,14 @@ export const createApp = async (args: DefaultedArgs): Promise<App> => {
|
||||
*
|
||||
* The address might be a URL or it might be a pipe or socket path.
|
||||
*/
|
||||
export const ensureAddress = (server: http.Server, protocol: string): URL | string => {
|
||||
export const ensureAddress = (server: http.Server, protocol: string, socketFd?: number): URL | string => {
|
||||
const addr = server.address()
|
||||
|
||||
// Node reports no address for a Unix socket adopted by fd.
|
||||
if (!addr && typeof socketFd === "number") {
|
||||
return `fd:${socketFd}`
|
||||
}
|
||||
|
||||
if (!addr) {
|
||||
throw new Error("Server has no address")
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export const runCodeServer = async (
|
||||
|
||||
const app = await createApp(args)
|
||||
const protocol = args.cert ? "https" : "http"
|
||||
const serverAddress = ensureAddress(app.server, protocol)
|
||||
const serverAddress = ensureAddress(app.server, protocol, args["socket-fd"])
|
||||
const { disposeRoutes, heart } = await register(app, args)
|
||||
|
||||
logger.info(`Using config file ${args.config}`)
|
||||
|
||||
@@ -8,6 +8,9 @@ import { paths } from "./util"
|
||||
|
||||
const timeoutInterval = 10000 // 10s, matches VS Code's timeouts.
|
||||
|
||||
// Fd 3 in the child is the IPC channel, so the inherited socket goes to fd 4.
|
||||
const CHILD_SOCKET_FD = 4
|
||||
|
||||
/**
|
||||
* Listen to a single message from a process. Reject if the process errors,
|
||||
* exits, or times out.
|
||||
@@ -334,13 +337,14 @@ export class ParentProcess extends Process {
|
||||
}
|
||||
|
||||
private spawn(): cp.ChildProcess {
|
||||
const socketFd = this.args?.["socket-fd"]
|
||||
return cp.fork(path.join(__dirname, "entry"), {
|
||||
env: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
NODE_EXEC_PATH: process.execPath,
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||
stdio: typeof socketFd === "number" ? ["pipe", "pipe", "pipe", "ipc", socketFd] : ["pipe", "pipe", "pipe", "ipc"],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,7 +363,8 @@ export class ParentProcess extends Process {
|
||||
this.logger,
|
||||
)
|
||||
this.logger.debug("got message", field("message", message))
|
||||
this.send(child, { type: "handshake", args: this.args })
|
||||
const args = typeof this.args["socket-fd"] === "number" ? { ...this.args, "socket-fd": CHILD_SOCKET_FD } : this.args
|
||||
this.send(child, { type: "handshake", args })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
80
test/integration/socketFd.test.ts
Normal file
80
test/integration/socketFd.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ChildProcess, spawn } from "child_process"
|
||||
import { promises as fs } from "fs"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import { clean, tmpdir } from "../utils/helpers"
|
||||
|
||||
describe("--socket-fd", () => {
|
||||
const testName = "socketFd"
|
||||
const codeServerPath = process.env.CODE_SERVER_PATH || path.resolve("../../release-standalone/bin/code-server")
|
||||
let tempDir: string
|
||||
let socketDir: string | undefined
|
||||
let child: ChildProcess | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
await clean(testName)
|
||||
tempDir = await tmpdir(testName)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (child?.pid && child.exitCode === null) {
|
||||
// Kill the process group so that the wrapper's inner process also stops.
|
||||
process.kill(-child.pid, "SIGKILL")
|
||||
}
|
||||
if (socketDir) {
|
||||
await fs.rm(socketDir, { force: true, recursive: true })
|
||||
socketDir = undefined
|
||||
}
|
||||
})
|
||||
|
||||
// Give the listener to code-server as fd 3, as systemd socket activation does.
|
||||
const getHealthzThroughFd = async (server: net.Server, target: http.RequestOptions): Promise<number> => {
|
||||
const fd: number = (server as any)._handle.fd
|
||||
let output = ""
|
||||
child = spawn(
|
||||
codeServerPath,
|
||||
[
|
||||
"--socket-fd=3",
|
||||
"--auth=none",
|
||||
`--user-data-dir=${path.join(tempDir, "data")}`,
|
||||
`--config=${path.join(tempDir, "config.yaml")}`,
|
||||
],
|
||||
{ detached: true, stdio: ["ignore", "pipe", "pipe", fd] },
|
||||
)
|
||||
child.stdout?.on("data", (chunk) => (output += chunk))
|
||||
child.stderr?.on("data", (chunk) => (output += chunk))
|
||||
await new Promise((resolve) => server.close(resolve))
|
||||
|
||||
const exited = new Promise<never>((_, reject) => {
|
||||
child?.once("exit", (code) => reject(new Error(`code-server exited with code ${code}:\n${output}`)))
|
||||
})
|
||||
const status = new Promise<number>((resolve, reject) => {
|
||||
http
|
||||
.get({ ...target, path: "/healthz" }, (res) => {
|
||||
res.resume()
|
||||
resolve(res.statusCode ?? 0)
|
||||
})
|
||||
.once("error", (error) => reject(new Error(`${error.message}:\n${output}`)))
|
||||
})
|
||||
return Promise.race([status, exited])
|
||||
}
|
||||
|
||||
it("should serve requests on a Unix socket passed as a file descriptor", async () => {
|
||||
// macOS limits socket paths to 104 bytes, and os.tmpdir() can be too long.
|
||||
socketDir = await fs.mkdtemp("/tmp/cs-fd-")
|
||||
const server = net.createServer()
|
||||
await new Promise<void>((resolve) => server.listen(path.join(socketDir!, "listen.sock"), resolve))
|
||||
// server.close() unlinks the socket path, so connect through a hard link.
|
||||
const socketPath = path.join(socketDir, "connect.sock")
|
||||
await fs.link(path.join(socketDir, "listen.sock"), socketPath)
|
||||
expect(await getHealthzThroughFd(server, { socketPath })).toBe(200)
|
||||
}, 60000)
|
||||
|
||||
it("should serve requests on a TCP socket passed as a file descriptor", async () => {
|
||||
const server = net.createServer()
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
|
||||
const { port } = server.address() as net.AddressInfo
|
||||
expect(await getHealthzThroughFd(server, { host: "127.0.0.1", port })).toBe(200)
|
||||
}, 60000)
|
||||
})
|
||||
@@ -152,6 +152,9 @@ describe("ensureAddress", () => {
|
||||
it("should throw and error if no address", () => {
|
||||
expect(() => ensureAddress(mockServer, "http")).toThrow("Server has no address")
|
||||
})
|
||||
it("should describe a --socket-fd listener by its fd when node reports no address", () => {
|
||||
expect(ensureAddress(mockServer, "http", 4)).toBe("fd:4")
|
||||
})
|
||||
it("should return the address if it's a string", async () => {
|
||||
mockServer.address = () => "/path/to/unix.sock"
|
||||
const address = ensureAddress(mockServer, "http")
|
||||
|
||||
Reference in New Issue
Block a user