mirror of
https://github.com/coder/code-server.git
synced 2026-09-02 08:30:17 +02:00
Add --socket-fd CLI option (#7940)
This commit is contained in:
39
docs/FAQ.md
39
docs/FAQ.md
@@ -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.
|
||||||
|
|||||||
@@ -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"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[]",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user