Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- [How do I disable the proxy?](#how-do-i-disable-the-proxy)
- [How do I disable file download?](#how-do-i-disable-file-download)
- [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 -->
<!-- prettier-ignore-end -->
Expand Down Expand Up @@ -560,3 +561,41 @@ To fix this, you must either:
create and trust a certificate manually).
- Disable security if your browser allows it. For example, in Chromium see
`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.
15 changes: 11 additions & 4 deletions src/node/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { EditorSessionManager, makeEditorSessionManagerServer } from "./vscodeSo
import { handleUpgrade } from "./wsRouter"

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 {
/** Handles regular HTTP requests. */
Expand All @@ -30,8 +31,12 @@ const isSocketOpts = (opts: ListenOptions): opts is SocketOptions => {
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) => {
if (isSocketOpts(opts)) {
if (!isFdOpts(opts) && isSocketOpts(opts)) {
try {
await fs.unlink(opts.socket)
} catch (error: any) {
Expand All @@ -46,7 +51,9 @@ export const listen = async (server: http.Server, opts: ListenOptions) => {
server.on("error", (err) => util.logError(logger, "http server error", err))
resolve()
}
if (isSocketOpts(opts)) {
if (isFdOpts(opts)) {
server.listen({ fd: opts["socket-fd"] }, onListen)
} else if (isSocketOpts(opts)) {
server.listen(opts.socket, onListen)
} else {
// [] is the correct format when using :: but Node errors with them.
Expand All @@ -56,7 +63,7 @@ export const listen = async (server: http.Server, opts: ListenOptions) => {

// NOTE@jsjoeio: we need to chmod after the server is finished
// listening. Otherwise, the socket may not have been created yet.
if (isSocketOpts(opts)) {
if (!isFdOpts(opts) && isSocketOpts(opts)) {
if (opts["socket-mode"]) {
await fs.chmod(opts.socket, opts["socket-mode"])
}
Expand Down
5 changes: 5 additions & 0 deletions src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs {
open?: boolean
"bind-addr"?: string
socket?: string
"socket-fd"?: number
"socket-mode"?: string
"trusted-origins"?: string[]
version?: boolean
Expand Down Expand Up @@ -235,6 +236,10 @@ export const options: Options<Required<UserProvidedArgs>> = {
port: { type: "number", description: "" },

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." },
"trusted-origins": {
type: "string[]",
Expand Down
46 changes: 45 additions & 1 deletion test/unit/node/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { promises } from "fs"
import * as http from "http"
import * as https from "https"
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 { generateCertificate } from "../../../src/node/util"
import { clean, mockLogger, getAvailablePort, tmpdir } from "../../utils/helpers"
Expand Down Expand Up @@ -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()
})
})