Skip to content

Commit 08b3775

Browse files
committed
Configuration file bug fixes based on @code-asher's review
1 parent 5a186de commit 08b3775

File tree

4 files changed

+151
-82
lines changed

4 files changed

+151
-82
lines changed

doc/FAQ.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,10 @@ code-server crashes can be helpful.
168168
### Where is the data directory?
169169

170170
If the `XDG_DATA_HOME` environment variable is set the data directory will be
171-
`$XDG_DATA_HOME/code-server`. Otherwise the default is `~/.local/share/code-server`.
172-
On Windows, it will be `%APPDATA%\Local\code-server\Data`.
171+
`$XDG_DATA_HOME/code-server`. Otherwise:
172+
173+
1. Unix: `~/.local/share/code-server`
174+
1. Windows: `%APPDATA%\Local\code-server\Data`
173175

174176
## Enterprise
175177

src/node/cli.ts

+103-37
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { field, Level, logger } from "@coder/logger"
12
import * as fs from "fs-extra"
23
import yaml from "js-yaml"
34
import * as path from "path"
4-
import { field, logger, Level } from "@coder/logger"
55
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
66
import { AuthType } from "./http"
7-
import { paths, uxPath } from "./util"
7+
import { generatePassword, humanPath, paths } from "./util"
88

99
export class Optional<T> {
1010
public constructor(public readonly value?: T) {}
@@ -84,7 +84,10 @@ type Options<T> = {
8484

8585
const options: Options<Required<Args>> = {
8686
auth: { type: AuthType, description: "The type of authentication to use." },
87-
password: { type: "string", description: "The password for password authentication." },
87+
password: {
88+
type: "string",
89+
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
90+
},
8891
cert: {
8992
type: OptionalString,
9093
path: true,
@@ -96,11 +99,14 @@ const options: Options<Required<Args>> = {
9699
json: { type: "boolean" },
97100
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
98101

99-
"bind-addr": { type: "string", description: "Address to bind to in host:port." },
102+
"bind-addr": {
103+
type: "string",
104+
description: "Address to bind to in host:port. You can also use $PORT to override the port.",
105+
},
100106

101107
config: {
102108
type: "string",
103-
description: "Path to yaml config file. Every flag maps directory to a key in the config file.",
109+
description: "Path to yaml config file. Every flag maps directly to a key in the config file.",
104110
},
105111

106112
// These two have been deprecated by bindAddr.
@@ -145,7 +151,19 @@ export const optionDescriptions = (): string[] => {
145151
)
146152
}
147153

148-
export const parse = (argv: string[]): Args => {
154+
export const parse = (
155+
argv: string[],
156+
opts?: {
157+
configFile: string
158+
},
159+
): Args => {
160+
const error = (msg: string): Error => {
161+
if (opts?.configFile) {
162+
msg = `error reading ${opts.configFile}: ${msg}`
163+
}
164+
return new Error(msg)
165+
}
166+
149167
const args: Args = { _: [] }
150168
let ended = false
151169

@@ -175,7 +193,11 @@ export const parse = (argv: string[]): Args => {
175193
}
176194

177195
if (!key || !options[key]) {
178-
throw new Error(`Unknown option ${arg}`)
196+
throw error(`Unknown option ${arg}`)
197+
}
198+
199+
if (key === "password" && !opts?.configFile) {
200+
throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
179201
}
180202

181203
const option = options[key]
@@ -194,7 +216,11 @@ export const parse = (argv: string[]): Args => {
194216
;(args[key] as OptionalString) = new OptionalString(value)
195217
continue
196218
} else if (!value) {
197-
throw new Error(`--${key} requires a value`)
219+
throw error(`--${key} requires a value`)
220+
}
221+
222+
if (option.type == OptionalString && value == "false") {
223+
continue
198224
}
199225

200226
if (option.path) {
@@ -214,15 +240,15 @@ export const parse = (argv: string[]): Args => {
214240
case "number":
215241
;(args[key] as number) = parseInt(value, 10)
216242
if (isNaN(args[key] as number)) {
217-
throw new Error(`--${key} must be a number`)
243+
throw error(`--${key} must be a number`)
218244
}
219245
break
220246
case OptionalString:
221247
;(args[key] as OptionalString) = new OptionalString(value)
222248
break
223249
default: {
224250
if (!Object.values(option.type).includes(value)) {
225-
throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
251+
throw error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
226252
}
227253
;(args[key] as string) = value
228254
break
@@ -284,53 +310,93 @@ export const parse = (argv: string[]): Args => {
284310
return args
285311
}
286312

287-
const defaultConfigFile = `
313+
async function defaultConfigFile(): Promise<string> {
314+
return `bind-addr: 127.0.0.1:8080
288315
auth: password
289-
bind-addr: 127.0.0.1:8080
290-
`.trimLeft()
291-
292-
// readConfigFile reads the config file specified in the config flag
293-
// and loads it's configuration.
294-
//
295-
// Flags set on the CLI take priority.
296-
//
297-
// The config file can also be passed via $CODE_SERVER_CONFIG and defaults
298-
// to ~/.config/code-server/config.yaml.
299-
export async function readConfigFile(args: Args): Promise<Args> {
300-
const configPath = getConfigPath(args)
316+
password: ${await generatePassword()}
317+
cert: false
318+
`
319+
}
320+
321+
/**
322+
* Reads the code-server yaml config file and returns it as Args.
323+
*
324+
* @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default.
325+
*/
326+
export async function readConfigFile(configPath?: string): Promise<Args> {
327+
if (!configPath) {
328+
configPath = process.env.CODE_SERVER_CONFIG
329+
if (!configPath) {
330+
configPath = path.join(paths.config, "config.yaml")
331+
}
332+
}
301333

302334
if (!(await fs.pathExists(configPath))) {
303-
await fs.outputFile(configPath, defaultConfigFile)
304-
logger.info(`Wrote default config file to ${uxPath(configPath)}`)
335+
await fs.outputFile(configPath, await defaultConfigFile())
336+
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
305337
}
306338

307-
logger.info(`Using config file from ${uxPath(configPath)}`)
339+
logger.info(`Using config file from ${humanPath(configPath)}`)
308340

309341
const configFile = await fs.readFile(configPath)
310342
const config = yaml.safeLoad(configFile.toString(), {
311-
filename: args.config,
343+
filename: configPath,
312344
})
313345

314346
// We convert the config file into a set of flags.
315347
// This is a temporary measure until we add a proper CLI library.
316348
const configFileArgv = Object.entries(config).map(([optName, opt]) => {
317-
if (opt === null) {
349+
if (opt === true) {
318350
return `--${optName}`
319351
}
320352
return `--${optName}=${opt}`
321353
})
322-
const configFileArgs = parse(configFileArgv)
354+
const args = parse(configFileArgv, {
355+
configFile: configPath,
356+
})
357+
return {
358+
...args,
359+
config: configPath,
360+
}
361+
}
323362

324-
// This prioritizes the flags set in args over the ones in the config file.
325-
return Object.assign(configFileArgs, args)
363+
function parseBindAddr(bindAddr: string): [string, number] {
364+
const u = new URL(`http://${bindAddr}`)
365+
return [u.hostname, parseInt(u.port, 10)]
326366
}
327367

328-
function getConfigPath(args: Args): string {
329-
if (args.config !== undefined) {
330-
return args.config
368+
interface Addr {
369+
host: string
370+
port: number
371+
}
372+
373+
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
374+
addr = { ...addr }
375+
if (args["bind-addr"]) {
376+
;[addr.host, addr.port] = parseBindAddr(args["bind-addr"])
331377
}
332-
if (process.env.CODE_SERVER_CONFIG !== undefined) {
333-
return process.env.CODE_SERVER_CONFIG
378+
if (args.host) {
379+
addr.host = args.host
334380
}
335-
return path.join(paths.config, "config.yaml")
381+
if (args.port !== undefined) {
382+
addr.port = args.port
383+
}
384+
return addr
385+
}
386+
387+
export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] {
388+
let addr: Addr = {
389+
host: "localhost",
390+
port: 8080,
391+
}
392+
393+
addr = bindAddrFromArgs(addr, configArgs)
394+
395+
if (process.env.PORT) {
396+
addr.port = parseInt(process.env.PORT, 10)
397+
}
398+
399+
addr = bindAddrFromArgs(addr, cliArgs)
400+
401+
return [addr.host, addr.port]
336402
}

src/node/entry.ts

+27-32
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { ProxyHttpProvider } from "./app/proxy"
99
import { StaticHttpProvider } from "./app/static"
1010
import { UpdateHttpProvider } from "./app/update"
1111
import { VscodeHttpProvider } from "./app/vscode"
12-
import { Args, optionDescriptions, parse, readConfigFile } from "./cli"
12+
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile } from "./cli"
1313
import { AuthType, HttpServer, HttpServerOptions } from "./http"
14-
import { generateCertificate, generatePassword, hash, open, uxPath } from "./util"
14+
import { generateCertificate, hash, open, humanPath } from "./util"
1515
import { ipcMain, wrap } from "./wrapper"
1616

1717
process.on("uncaughtException", (error) => {
@@ -31,35 +31,24 @@ try {
3131
const version = pkg.version || "development"
3232
const commit = pkg.commit || "development"
3333

34-
const main = async (args: Args): Promise<void> => {
35-
args = await readConfigFile(args)
34+
const main = async (cliArgs: Args): Promise<void> => {
35+
const configArgs = await readConfigFile(cliArgs.config)
36+
// This prioritizes the flags set in args over the ones in the config file.
37+
let args = Object.assign(configArgs, cliArgs)
3638

37-
if (args.verbose === true) {
38-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
39-
logger.info(`Using extensions-dir at ${uxPath(args["extensions-dir"]!)}`)
40-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41-
logger.info(`Using user-data-dir at ${uxPath(args["user-data-dir"]!)}`)
42-
}
43-
44-
const auth = args.auth || AuthType.Password
45-
const generatedPassword = (args.password || process.env.PASSWORD) !== ""
46-
const password = auth === AuthType.Password && (args.password || process.env.PASSWORD || (await generatePassword()))
39+
logger.trace(`Using extensions-dir at ${humanPath(args["extensions-dir"])}`)
40+
logger.trace(`Using user-data-dir at ${humanPath(args["user-data-dir"])}`)
4741

48-
let host = args.host
49-
let port = args.port
50-
if (args["bind-addr"] !== undefined) {
51-
const u = new URL(`http://${args["bind-addr"]}`)
52-
host = u.hostname
53-
port = parseInt(u.port, 10)
54-
}
42+
const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password)
43+
const [host, port] = bindAddrFromAllSources(cliArgs, configArgs)
5544

5645
// Spawn the main HTTP server.
5746
const options: HttpServerOptions = {
58-
auth,
47+
auth: args.auth,
5948
commit,
60-
host: host || (args.auth === AuthType.Password && args.cert !== undefined ? "0.0.0.0" : "localhost"),
49+
host: host,
6150
password: password ? hash(password) : undefined,
62-
port: port !== undefined ? port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
51+
port: port,
6352
proxyDomains: args["proxy-domain"],
6453
socket: args.socket,
6554
...(args.cert && !args.cert.value
@@ -77,7 +66,7 @@ const main = async (args: Args): Promise<void> => {
7766
const httpServer = new HttpServer(options)
7867
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
7968
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
80-
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, true)
69+
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
8170
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
8271
httpServer.registerHttpProvider("/login", LoginHttpProvider)
8372
httpServer.registerHttpProvider("/static", StaticHttpProvider)
@@ -89,14 +78,20 @@ const main = async (args: Args): Promise<void> => {
8978
const serverAddress = await httpServer.listen()
9079
logger.info(`HTTP server listening on ${serverAddress}`)
9180

92-
if (auth === AuthType.Password && generatedPassword) {
93-
logger.info(` - Password is ${password}`)
94-
logger.info(" - To use your own password set it in the config file with the password key or use $PASSWORD")
95-
if (!args.auth) {
96-
logger.info(" - To disable use `--auth none`")
81+
if (!args.auth) {
82+
args = {
83+
...args,
84+
auth: AuthType.None,
85+
}
86+
}
87+
88+
if (args.auth === AuthType.Password) {
89+
if (process.env.PASSWORD) {
90+
logger.info(" - Using password from $PASSWORD")
91+
} else {
92+
logger.info(` - Using password from ${humanPath(args.config)}`)
9793
}
98-
} else if (auth === AuthType.Password) {
99-
logger.info(" - Using custom password for authentication")
94+
logger.info(" - To disable use `--auth none`")
10095
} else {
10196
logger.info(" - No authentication")
10297
}

src/node/util.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,20 @@ interface Paths {
1616

1717
export const paths = getEnvPaths()
1818

19-
// getEnvPaths gets the config and data paths for the current platform/configuration.
20-
//
21-
// On MacOS this function gets the standard XDG directories instead of using the native macOS
22-
// ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories.
19+
/**
20+
* Gets the config and data paths for the current platform/configuration.
21+
* On MacOS this function gets the standard XDG directories instead of using the native macOS
22+
* ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories.
23+
*/
2324
function getEnvPaths(): Paths {
2425
let paths: Paths
2526
if (process.platform === "win32") {
2627
paths = envPaths("code-server", {
2728
suffix: "",
2829
})
2930
} else {
30-
if (xdgBasedir.data === undefined) {
31-
throw new Error("Missing data directory?")
32-
}
33-
if (xdgBasedir.config === undefined) {
34-
throw new Error("Missing config directory?")
31+
if (xdgBasedir.data === undefined || xdgBasedir.config === undefined) {
32+
throw new Error("No home folder?")
3533
}
3634
paths = {
3735
data: path.join(xdgBasedir.data, "code-server"),
@@ -42,8 +40,16 @@ function getEnvPaths(): Paths {
4240
return paths
4341
}
4442

45-
// uxPath replaces the home directory in p with ~.
46-
export function uxPath(p: string): string {
43+
/**
44+
* humanPath replaces the home directory in p with ~.
45+
* Makes it more readable.
46+
*
47+
* @param p
48+
*/
49+
export function humanPath(p?: string): string {
50+
if (!p) {
51+
return ""
52+
}
4753
return p.replace(os.homedir(), "~")
4854
}
4955

0 commit comments

Comments
 (0)