Add --socket-fd CLI option (#7940)

This commit is contained in:
Robert Gingras
2026-08-20 17:24:49 -04:00
committed by GitHub
parent 2d5dbf0b77
commit 4cb856c475
4 changed files with 100 additions and 5 deletions

View File

@@ -41,6 +41,7 @@
- [How do I disable the proxy?](#how-do-i-disable-the-proxy) - [How do I disable the proxy?](#how-do-i-disable-the-proxy)
- [How do I disable file download?](#how-do-i-disable-file-download) - [How do I disable file download?](#how-do-i-disable-file-download)
- [Why do web views not work?](#why-do-web-views-not-work) - [Why do web views not work?](#why-do-web-views-not-work)
- [Can I run code-server with systemd socket activation?](#can-i-run-code-server-with-systemd-socket-activation)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
@@ -560,3 +561,41 @@ To fix this, you must either:
create and trust a certificate manually). create and trust a certificate manually).
- Disable security if your browser allows it. For example, in Chromium see - Disable security if your browser allows it. For example, in Chromium see
`chrome://flags/#unsafely-treat-insecure-origin-as-secure` `chrome://flags/#unsafely-treat-insecure-origin-as-secure`
## Can I run code-server with systemd socket activation?
Yes. Pass the inherited socket to code-server with `--socket-fd`. systemd
passes the first listening socket as file descriptor `3`.
Create a socket unit, `~/.config/systemd/user/code-server.socket`:
```ini
[Socket]
ListenStream=8080
[Install]
WantedBy=sockets.target
```
And a matching service unit, `~/.config/systemd/user/code-server.service`:
```ini
[Service]
ExecStart=/usr/bin/code-server --socket-fd 3
```
Then enable and start the socket:
```bash
systemctl --user enable --now code-server.socket
```
code-server will start on the first connection and listen on the socket
systemd created. `--socket-fd` takes precedence over `--socket` and
`--bind-addr`/`--port`/`--host`, and `--socket-mode` is ignored because
systemd owns the socket's permissions.
Socket activation only changes how code-server binds; your usual
authentication still applies (it keeps prompting for the configured password
unless you set `--auth none`), so keep authentication enabled when exposing the
server.

View File

@@ -13,7 +13,8 @@ import { EditorSessionManager, makeEditorSessionManagerServer } from "./vscodeSo
import { handleUpgrade } from "./wsRouter" import { handleUpgrade } from "./wsRouter"
type SocketOptions = { socket: string; "socket-mode"?: string } type SocketOptions = { socket: string; "socket-mode"?: string }
type ListenOptions = DefaultedArgs | SocketOptions type FdOptions = { "socket-fd": number }
type ListenOptions = DefaultedArgs | SocketOptions | FdOptions
export interface App extends Disposable { export interface App extends Disposable {
/** Handles regular HTTP requests. */ /** Handles regular HTTP requests. */
@@ -30,8 +31,12 @@ const isSocketOpts = (opts: ListenOptions): opts is SocketOptions => {
return !!(opts as SocketOptions).socket || !(opts as DefaultedArgs).host return !!(opts as SocketOptions).socket || !(opts as DefaultedArgs).host
} }
export const isFdOpts = (opts: ListenOptions): opts is FdOptions => {
return typeof (opts as FdOptions)["socket-fd"] === "number"
}
export const listen = async (server: http.Server, opts: ListenOptions) => { export const listen = async (server: http.Server, opts: ListenOptions) => {
if (isSocketOpts(opts)) { if (!isFdOpts(opts) && isSocketOpts(opts)) {
try { try {
await fs.unlink(opts.socket) await fs.unlink(opts.socket)
} catch (error: any) { } catch (error: any) {
@@ -46,7 +51,9 @@ export const listen = async (server: http.Server, opts: ListenOptions) => {
server.on("error", (err) => util.logError(logger, "http server error", err)) server.on("error", (err) => util.logError(logger, "http server error", err))
resolve() resolve()
} }
if (isSocketOpts(opts)) { if (isFdOpts(opts)) {
server.listen({ fd: opts["socket-fd"] }, onListen)
} else if (isSocketOpts(opts)) {
server.listen(opts.socket, onListen) server.listen(opts.socket, onListen)
} else { } else {
// [] is the correct format when using :: but Node errors with them. // [] is the correct format when using :: but Node errors with them.
@@ -56,7 +63,7 @@ export const listen = async (server: http.Server, opts: ListenOptions) => {
// NOTE@jsjoeio: we need to chmod after the server is finished // NOTE@jsjoeio: we need to chmod after the server is finished
// listening. Otherwise, the socket may not have been created yet. // listening. Otherwise, the socket may not have been created yet.
if (isSocketOpts(opts)) { if (!isFdOpts(opts) && isSocketOpts(opts)) {
if (opts["socket-mode"]) { if (opts["socket-mode"]) {
await fs.chmod(opts.socket, opts["socket-mode"]) await fs.chmod(opts.socket, opts["socket-mode"])
} }

View File

@@ -83,6 +83,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs {
open?: boolean open?: boolean
"bind-addr"?: string "bind-addr"?: string
socket?: string socket?: string
"socket-fd"?: number
"socket-mode"?: string "socket-mode"?: string
"trusted-origins"?: string[] "trusted-origins"?: string[]
version?: boolean version?: boolean
@@ -236,6 +237,10 @@ export const options: Options<Required<UserProvidedArgs>> = {
port: { type: "number", description: "" }, port: { type: "number", description: "" },
socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." }, socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." },
"socket-fd": {
type: "number",
description: "File descriptor of a pre-bound, listening socket to use (for systemd socket activation).",
},
"socket-mode": { type: "string", description: "File mode of the socket." }, "socket-mode": { type: "string", description: "File mode of the socket." },
"trusted-origins": { "trusted-origins": {
type: "string[]", type: "string[]",

View File

@@ -3,7 +3,7 @@ import { promises } from "fs"
import * as http from "http" import * as http from "http"
import * as https from "https" import * as https from "https"
import * as path from "path" import * as path from "path"
import { createApp, ensureAddress, handleArgsSocketCatchError, listen } from "../../../src/node/app" import { createApp, ensureAddress, handleArgsSocketCatchError, isFdOpts, listen } from "../../../src/node/app"
import { OptionalString, setDefaults } from "../../../src/node/cli" import { OptionalString, setDefaults } from "../../../src/node/cli"
import { generateCertificate } from "../../../src/node/util" import { generateCertificate } from "../../../src/node/util"
import { clean, mockLogger, getAvailablePort, tmpdir } from "../../utils/helpers" import { clean, mockLogger, getAvailablePort, tmpdir } from "../../utils/helpers"
@@ -261,3 +261,47 @@ describe("listen", () => {
} }
}) })
}) })
describe("listen (socket-fd)", () => {
// Wrap a bound-but-not-yet-listening TCP socket so we get a real file
// descriptor that listen({ fd }) can adopt, mirroring the systemd socket
// activation case where the process inherits an fd and calls listen(2) on it.
// Using a live net.Server's fd instead fails with EEXIST because the socket
// is already listening in-process.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { TCP, constants: TCPConstants } = (process as any).binding("tcp_wrap")
let inherited: any
let httpServer: http.Server
let unlinkSpy: jest.SpyInstance
beforeEach(async () => {
mockLogger()
unlinkSpy = jest.spyOn(promises, "unlink")
inherited = new TCP(TCPConstants.SERVER)
inherited.bind("127.0.0.1", 0)
httpServer = http.createServer()
})
afterEach(() => {
httpServer.close()
try {
inherited.close()
} catch {
// The fd is adopted by httpServer.close() above; ignore double-close.
}
jest.clearAllMocks()
})
it("isFdOpts detects a numeric socket-fd", () => {
expect(isFdOpts({ "socket-fd": 3 })).toBe(true)
expect(isFdOpts({ socket: "/tmp/x.sock" } as any)).toBe(false)
})
it("listens on an inherited fd without unlinking", async () => {
const fd = inherited.fd as number
await listen(httpServer, { "socket-fd": fd })
expect(httpServer.address()).not.toBeNull()
expect(unlinkSpy).not.toHaveBeenCalled()
})
})