diff --git a/doc/FAQ.md b/doc/FAQ.md index ec0acf93..cc619730 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -168,10 +168,8 @@ code-server crashes can be helpful. ### Where is the data directory? If the `XDG_DATA_HOME` environment variable is set the data directory will be -`$XDG_DATA_HOME/code-server`. Otherwise the default is: - -1. Linux: `~/.local/share/code-server`. -2. Mac: `~/Library/Application\ Support/code-server`. +`$XDG_DATA_HOME/code-server`. Otherwise the default is `~/.local/share/code-server`. +On Windows, it will be `%APPDATA%\Local\code-server\Data`. ## Enterprise diff --git a/package.json b/package.json index f1813e89..c86e0174 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dependencies": { "@coder/logger": "1.1.11", "adm-zip": "^0.4.14", + "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", diff --git a/src/node/cli.ts b/src/node/cli.ts index f135d32a..53561d25 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -4,8 +4,7 @@ import * as path from "path" import { field, logger, Level } from "@coder/logger" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" import { AuthType } from "./http" -import { xdgLocalDir } from "./util" -import xdgBasedir from "xdg-basedir" +import { paths, uxPath } from "./util" export class Optional { public constructor(public readonly value?: T) {} @@ -272,7 +271,7 @@ export const parse = (argv: string[]): Args => { } if (!args["user-data-dir"]) { - args["user-data-dir"] = xdgLocalDir + args["user-data-dir"] = paths.data } if (!args["extensions-dir"]) { @@ -282,6 +281,11 @@ export const parse = (argv: string[]): Args => { return args } +const defaultConfigFile = ` +auth: password +bind-addr: 127.0.0.1:8080 +`.trimLeft() + // readConfigFile reads the config file specified in the config flag // and loads it's configuration. // @@ -291,14 +295,14 @@ export const parse = (argv: string[]): Args => { // to ~/.config/code-server/config.yaml. export async function readConfigFile(args: Args): Promise { const configPath = getConfigPath(args) - if (configPath === undefined) { - return args - } if (!(await fs.pathExists(configPath))) { - await fs.outputFile(configPath, `default: hello`) + await fs.outputFile(configPath, defaultConfigFile) + logger.info(`Wrote default config file to ${uxPath(configPath)}`) } + logger.info(`Using config file from ${uxPath(configPath)}`) + const configFile = await fs.readFile(configPath) const config = yaml.safeLoad(configFile.toString(), { filename: args.config, @@ -306,22 +310,24 @@ export async function readConfigFile(args: Args): Promise { // We convert the config file into a set of flags. // This is a temporary measure until we add a proper CLI library. - const configFileArgv = Object.entries(config).map(([optName, opt]) => `--${optName}=${opt}`) + const configFileArgv = Object.entries(config).map(([optName, opt]) => { + if (opt === null) { + return `--${optName}` + } + return `--${optName}=${opt}` + }) const configFileArgs = parse(configFileArgv) // This prioritizes the flags set in args over the ones in the config file. return Object.assign(configFileArgs, args) } -function getConfigPath(args: Args): string | undefined { +function getConfigPath(args: Args): string { if (args.config !== undefined) { return args.config } if (process.env.CODE_SERVER_CONFIG !== undefined) { return process.env.CODE_SERVER_CONFIG } - if (xdgBasedir.config !== undefined) { - return `${xdgBasedir.config}/code-server/config.yaml` - } - return undefined + return path.join(paths.config, "config.yaml") } diff --git a/src/node/entry.ts b/src/node/entry.ts index 493d2689..f048ec9a 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -11,7 +11,7 @@ import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { Args, optionDescriptions, parse, readConfigFile } from "./cli" import { AuthType, HttpServer, HttpServerOptions } from "./http" -import { generateCertificate, generatePassword, hash, open } from "./util" +import { generateCertificate, generatePassword, hash, open, uxPath } from "./util" import { ipcMain, wrap } from "./wrapper" process.on("uncaughtException", (error) => { @@ -34,6 +34,11 @@ const commit = pkg.commit || "development" const main = async (args: Args): Promise => { args = await readConfigFile(args) + if (args.verbose === true) { + logger.info(`Using extensions-dir at ${uxPath(args["extensions-dir"]!)}`) + logger.info(`Using user-data-dir at ${uxPath(args["user-data-dir"]!)}`) + } + const auth = args.auth || AuthType.Password const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) diff --git a/src/node/http.ts b/src/node/http.ts index 5c065374..96f074a6 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -14,7 +14,7 @@ import * as url from "url" import { HttpCode, HttpError } from "../common/http" import { normalize, Options, plural, split } from "../common/util" import { SocketProxyProvider } from "./socket" -import { getMediaMime, xdgLocalDir } from "./util" +import { getMediaMime, paths } from "./util" export type Cookies = { [key: string]: string[] | undefined } export type PostData = { [key: string]: string | string[] | undefined } @@ -473,7 +473,7 @@ export class HttpServer { public constructor(private readonly options: HttpServerOptions) { this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, ""))) - this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => { + this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => { const connections = await this.getConnections() logger.trace(`${connections} active connection${plural(connections)}`) return connections !== 0 diff --git a/src/node/settings.ts b/src/node/settings.ts index 0d6152b1..32166ddb 100644 --- a/src/node/settings.ts +++ b/src/node/settings.ts @@ -1,6 +1,6 @@ import * as fs from "fs-extra" import * as path from "path" -import { extend, xdgLocalDir } from "./util" +import { extend, paths } from "./util" import { logger } from "@coder/logger" export type Settings = { [key: string]: Settings | string | boolean | number } @@ -60,4 +60,4 @@ export interface CoderSettings extends UpdateSettings { /** * Global code-server settings file. */ -export const settings = new SettingsProvider(path.join(xdgLocalDir, "coder.json")) +export const settings = new SettingsProvider(path.join(paths.data, "coder.json")) diff --git a/src/node/util.ts b/src/node/util.ts index 44e27be0..72d3332f 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -4,24 +4,48 @@ import * as fs from "fs-extra" import * as os from "os" import * as path from "path" import * as util from "util" +import envPaths from "env-paths" +import xdgBasedir from "xdg-basedir" export const tmpdir = path.join(os.tmpdir(), "code-server") -const getXdgDataDir = (): string => { - switch (process.platform) { - case "win32": - return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), "AppData/Local"), "code-server/Data") - case "darwin": - return path.join( - process.env.XDG_DATA_HOME || path.join(os.homedir(), "Library/Application Support"), - "code-server", - ) - default: - return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"), "code-server") - } +interface Paths { + data: string + config: string } -export const xdgLocalDir = getXdgDataDir() +export const paths = getEnvPaths() + +// getEnvPaths gets the config and data paths for the current platform/configuration. +// +// On MacOS this function gets the standard XDG directories instead of using the native macOS +// ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories. +function getEnvPaths(): Paths { + let paths: Paths + if (process.platform === "win32") { + paths = envPaths("code-server", { + suffix: "", + }) + } else { + if (xdgBasedir.data === undefined) { + throw new Error("Missing data directory?") + } + if (xdgBasedir.config === undefined) { + throw new Error("Missing config directory?") + } + paths = { + data: path.join(xdgBasedir.data, "code-server"), + config: path.join(xdgBasedir.config, "code-server"), + } + } + + return paths +} + +// uxPath replaces the home directory in p with ~. +export function uxPath(p: string): string { + return p.replace(os.homedir(), "~") +} export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => { const paths = { diff --git a/test/cli.test.ts b/test/cli.test.ts index 4f1c2bfb..52c9fb61 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -2,7 +2,7 @@ import { logger, Level } from "@coder/logger" import * as assert from "assert" import * as path from "path" import { parse } from "../src/node/cli" -import { xdgLocalDir } from "../src/node/util" +import { paths } from "../src/node/util" describe("cli", () => { beforeEach(() => { @@ -12,8 +12,8 @@ describe("cli", () => { // The parser will always fill these out. const defaults = { _: [], - "extensions-dir": path.join(xdgLocalDir, "extensions"), - "user-data-dir": xdgLocalDir, + "extensions-dir": path.join(paths.data, "extensions"), + "user-data-dir": paths.data, } it("should set defaults", () => { diff --git a/yarn.lock b/yarn.lock index 470f299f..782882fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2562,6 +2562,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== +env-paths@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" + integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== + envinfo@^7.3.1: version "7.5.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.5.1.tgz#93c26897225a00457c75e734d354ea9106a72236"