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
|
## 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
|
## [4.143.1](https://github.com/coder/code-server/releases/tag/v4.143.1) - 2026-10-23
|
||||||
|
|
||||||
Code v1.143.1
|
Code v1.143.1
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1726,9 +1726,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/basic-ftp": {
|
"node_modules/basic-ftp": {
|
||||||
"version": "5.3.1",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.3.tgz",
|
||||||
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
|
"integrity": "sha512-T7GYtQ2eKJUN5YORG7A8u2WfB9JMN0a4/wl3MI64qq2qTRfa3FXzoNq0DDojZDkDyStcL+TkfKsz4JHEZx5kGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
@@ -90,6 +90,9 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "24.x"
|
"@types/node": "24.x"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"basic-ftp": "^6.2.1"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"code-server": "out/node/entry.js"
|
"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.
|
* 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()
|
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) {
|
if (!addr) {
|
||||||
throw new Error("Server has no address")
|
throw new Error("Server has no address")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const runCodeServer = async (
|
|||||||
|
|
||||||
const app = await createApp(args)
|
const app = await createApp(args)
|
||||||
const protocol = args.cert ? "https" : "http"
|
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)
|
const { disposeRoutes, heart } = await register(app, args)
|
||||||
|
|
||||||
logger.info(`Using config file ${args.config}`)
|
logger.info(`Using config file ${args.config}`)
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { paths } from "./util"
|
|||||||
|
|
||||||
const timeoutInterval = 10000 // 10s, matches VS Code's timeouts.
|
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,
|
* Listen to a single message from a process. Reject if the process errors,
|
||||||
* exits, or times out.
|
* exits, or times out.
|
||||||
@@ -334,13 +337,14 @@ export class ParentProcess extends Process {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private spawn(): cp.ChildProcess {
|
private spawn(): cp.ChildProcess {
|
||||||
|
const socketFd = this.args?.["socket-fd"]
|
||||||
return cp.fork(path.join(__dirname, "entry"), {
|
return cp.fork(path.join(__dirname, "entry"), {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||||
NODE_EXEC_PATH: process.execPath,
|
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,
|
||||||
)
|
)
|
||||||
this.logger.debug("got message", field("message", message))
|
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", () => {
|
it("should throw and error if no address", () => {
|
||||||
expect(() => ensureAddress(mockServer, "http")).toThrow("Server has 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 () => {
|
it("should return the address if it's a string", async () => {
|
||||||
mockServer.address = () => "/path/to/unix.sock"
|
mockServer.address = () => "/path/to/unix.sock"
|
||||||
const address = ensureAddress(mockServer, "http")
|
const address = ensureAddress(mockServer, "http")
|
||||||
|
|||||||
Reference in New Issue
Block a user