cli.ts 19.9 KB
Newer Older
1
import { field, Level, logger } from "@coder/logger"
A
Asher 已提交
2
import { promises as fs } from "fs"
A
Anmol Sethi 已提交
3
import yaml from "js-yaml"
4
import * as os from "os"
5
import * as path from "path"
A
Asher 已提交
6
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
7

A
Asher 已提交
8
export enum Feature {
9 10
  // No current experimental features!
  Placeholder = "placeholder",
A
Asher 已提交
11 12
}

A
Asher 已提交
13 14 15 16 17
export enum AuthType {
  Password = "password",
  None = "none",
}

A
Asher 已提交
18 19 20 21
export class Optional<T> {
  public constructor(public readonly value?: T) {}
}

A
Asher 已提交
22 23 24 25 26 27 28 29
export enum LogLevel {
  Trace = "trace",
  Debug = "debug",
  Info = "info",
  Warn = "warn",
  Error = "error",
}

A
Asher 已提交
30 31
export class OptionalString extends Optional<string> {}

32 33 34
export interface Args
  extends Pick<
    CodeServerLib.NativeParsedArgs,
35
    | "_"
36 37 38 39 40 41 42 43 44 45
    | "user-data-dir"
    | "enable-proposed-api"
    | "extensions-dir"
    | "builtin-extensions-dir"
    | "extra-extensions-dir"
    | "extra-builtin-extensions-dir"
    | "ignore-last-opened"
    | "locale"
    | "log"
    | "verbose"
46 47 48 49 50 51
    | "install-source"
    | "list-extensions"
    | "install-extension"
    | "uninstall-extension"
    | "locate-extension"
    // | "telemetry"
52
  > {
A
Asher 已提交
53 54 55
  config?: string
  auth?: AuthType
  password?: string
56
  "hashed-password"?: string
A
Asher 已提交
57
  cert?: OptionalString
A
Asher 已提交
58
  "cert-host"?: string
A
Asher 已提交
59 60
  "cert-key"?: string
  "disable-telemetry"?: boolean
A
Anmol Sethi 已提交
61
  "disable-update-check"?: boolean
A
Asher 已提交
62
  enable?: string[]
A
Asher 已提交
63 64 65
  help?: boolean
  host?: string
  json?: boolean
A
Asher 已提交
66
  log?: LogLevel
A
Asher 已提交
67 68 69 70 71 72 73 74 75 76 77 78
  open?: boolean
  port?: number
  "bind-addr"?: string
  socket?: string
  version?: boolean
  force?: boolean
  "show-versions"?: boolean
  "proxy-domain"?: string[]
  "reuse-window"?: boolean
  "new-window"?: boolean

  link?: OptionalString
A
Asher 已提交
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
}

interface Option<T> {
  type: T
  /**
   * Short flag for the option.
   */
  short?: string
  /**
   * Whether the option is a path and should be resolved.
   */
  path?: boolean
  /**
   * Description of the option. Leave blank to hide the option.
   */
  description?: string
A
Anmol Sethi 已提交
95 96

  /**
A
Anmol Sethi 已提交
97
   * If marked as beta, the option is marked as beta in help.
A
Anmol Sethi 已提交
98
   */
99
  beta?: boolean
A
Asher 已提交
100 101 102 103 104 105
}

type OptionType<T> = T extends boolean
  ? "boolean"
  : T extends OptionalString
  ? typeof OptionalString
A
Asher 已提交
106 107
  : T extends LogLevel
  ? typeof LogLevel
A
Asher 已提交
108 109 110 111 112 113 114 115 116 117 118 119
  : T extends AuthType
  ? typeof AuthType
  : T extends number
  ? "number"
  : T extends string
  ? "string"
  : T extends string[]
  ? "string[]"
  : "unknown"

type Options<T> = {
  [P in keyof T]: Option<OptionType<T[P]>>
120 121
}

A
Asher 已提交
122 123
const options: Options<Required<Args>> = {
  auth: { type: AuthType, description: "The type of authentication to use." },
124 125 126 127
  password: {
    type: "string",
    description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
  },
128
  "hashed-password": {
S
SPGoding 已提交
129 130
    type: "string",
    description:
131
      "The password hashed with argon2 for password authentication (can only be passed in via $HASHED_PASSWORD or the config file). \n" +
S
SPGoding 已提交
132 133
      "Takes precedence over 'password'.",
  },
A
Asher 已提交
134 135 136
  cert: {
    type: OptionalString,
    path: true,
137 138 139 140 141
    description: "Path to certificate. A self signed certificate is generated if none is provided.",
  },
  "cert-host": {
    type: "string",
    description: "Hostname to use when generating a self signed certificate.",
A
Asher 已提交
142 143
  },
  "cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
A
Asher 已提交
144
  "disable-telemetry": { type: "boolean", description: "Disable telemetry." },
A
Anmol Sethi 已提交
145 146 147 148 149 150
  "disable-update-check": {
    type: "boolean",
    description:
      "Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" +
      "then notifies you once every week that a new release is available.",
  },
A
Asher 已提交
151 152 153
  // --enable can be used to enable experimental features. These features
  // provide no guarantees.
  enable: { type: "string[]" },
A
Asher 已提交
154 155
  help: { type: "boolean", short: "h", description: "Show this output." },
  json: { type: "boolean" },
A
Asher 已提交
156
  open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
157

158 159 160 161
  "bind-addr": {
    type: "string",
    description: "Address to bind to in host:port. You can also use $PORT to override the port.",
  },
162

163 164
  config: {
    type: "string",
165
    description: "Path to yaml config file. Every flag maps directly to a key in the config file.",
166
  },
A
Anmol Sethi 已提交
167

168 169 170 171
  // These two have been deprecated by bindAddr.
  host: { type: "string", description: "" },
  port: { type: "number", description: "" },

A
Anmol Sethi 已提交
172
  socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." },
A
Asher 已提交
173 174 175 176 177 178 179 180
  version: { type: "boolean", short: "v", description: "Display version information." },
  _: { type: "string[]" },

  "user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
  "extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
  "builtin-extensions-dir": { type: "string", path: true },
  "extra-extensions-dir": { type: "string[]", path: true },
  "extra-builtin-extensions-dir": { type: "string[]", path: true },
181 182
  "list-extensions": { type: "boolean", description: "List installed VS Code extensions." },
  force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." },
183 184
  "install-source": { type: "string" },
  "locate-extension": { type: "string[]" },
A
Asher 已提交
185 186
  "install-extension": {
    type: "string[]",
A
Anmol Sethi 已提交
187 188 189
    description:
      "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" +
      "To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
A
Asher 已提交
190
  },
A
Asher 已提交
191 192 193 194 195
  "enable-proposed-api": {
    type: "string[]",
    description:
      "Enable proposed API features for extensions. Can receive one or more extension IDs to enable individually.",
  },
196 197
  "uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
  "show-versions": { type: "boolean", description: "Show VS Code extension versions." },
A
Asher 已提交
198
  "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
199 200 201
  "ignore-last-opened": {
    type: "boolean",
    short: "e",
A
Asher 已提交
202
    description: "Ignore the last opened directory or workspace in favor of an empty window.",
203
  },
204 205 206
  "new-window": {
    type: "boolean",
    short: "n",
A
Asher 已提交
207
    description: "Force to open a new window.",
208 209 210 211
  },
  "reuse-window": {
    type: "boolean",
    short: "r",
A
Asher 已提交
212
    description: "Force to open a file or folder in an already opened window.",
213 214
  },

A
Asher 已提交
215
  locale: { type: "string" },
A
Asher 已提交
216
  log: { type: LogLevel },
A
Asher 已提交
217
  verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
A
Anmol Sethi 已提交
218

A
Anmol Sethi 已提交
219
  link: {
220
    type: OptionalString,
221
    description: `
B
Ben Potter 已提交
222 223
      Securely bind code-server via our cloud service with the passed name. You'll get a URL like
      https://hostname-username.cdr.co at which you can easily access your code-server instance.
A
Anmol Sethi 已提交
224 225
      Authorization is done via GitHub.
    `,
A
Anmol Sethi 已提交
226
    beta: true,
227
  },
A
Asher 已提交
228 229 230 231 232 233 234 235 236
}

export const optionDescriptions = (): string[] => {
  const entries = Object.entries(options).filter(([, v]) => !!v.description)
  const widths = entries.reduce(
    (prev, [k, v]) => ({
      long: k.length > prev.long ? k.length : prev.long,
      short: v.short && v.short.length > prev.short ? v.short.length : prev.short,
    }),
A
Anmol Sethi 已提交
237
    { short: 0, long: 0 },
A
Asher 已提交
238
  )
A
Anmol Sethi 已提交
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
  return entries.map(([k, v]) => {
    const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k} `
    return (
      help +
      v.description
        ?.trim()
        .split(/\n/)
        .map((line, i) => {
          line = line.trim()
          if (i === 0) {
            return " ".repeat(widths.long - k.length) + (v.beta ? "(beta) " : "") + line
          }
          return " ".repeat(widths.long + widths.short + 6) + line
        })
        .join("\n") +
      (typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "")
    )
  })
A
Asher 已提交
257 258
}

259 260 261 262 263 264 265
export function splitOnFirstEquals(str: string): string[] {
  // we use regex instead of "=" to ensure we split at the first
  // "=" and return the following substring with it
  // important for the hashed-password which looks like this
  // $argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY
  // 2 means return two items
  // Source: https://stackoverflow.com/a/4607799/3015595
266 267
  // We use the ? to say the the substr after the = is optional
  const split = str.split(/=(.+)?/, 2)
268 269 270 271

  return split
}

272
export const parse = (
273 274
  argv: string[],
  opts?: {
275
    configFile?: string
276
  },
277
): Args => {
278 279 280 281
  const error = (msg: string): Error => {
    if (opts?.configFile) {
      msg = `error reading ${opts.configFile}: ${msg}`
    }
282

283 284 285
    return new Error(msg)
  }

A
Asher 已提交
286 287 288 289 290 291 292
  const args: Args = { _: [] }
  let ended = false

  for (let i = 0; i < argv.length; ++i) {
    const arg = argv[i]

    // -- signals the end of option parsing.
A
Asher 已提交
293
    if (!ended && arg === "--") {
A
Asher 已提交
294 295 296
      ended = true
      continue
    }
297

A
Asher 已提交
298 299 300
    // Options start with a dash and require a value if non-boolean.
    if (!ended && arg.startsWith("-")) {
      let key: keyof Args | undefined
301
      let value: string | undefined
A
Asher 已提交
302
      if (arg.startsWith("--")) {
303
        const split = splitOnFirstEquals(arg.replace(/^--/, ""))
304 305
        key = split[0] as keyof Args
        value = split[1]
306 307 308 309 310 311 312
      } else {
        const short = arg.replace(/^-/, "")
        const pair = Object.entries(options).find(([, v]) => v.short === short)
        if (pair) {
          key = pair[0] as keyof Args
        }
      }
A
Asher 已提交
313 314

      if (!key || !options[key]) {
315 316 317 318 319
        throw error(`Unknown option ${arg}`)
      }

      if (key === "password" && !opts?.configFile) {
        throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
A
Asher 已提交
320 321
      }

322 323
      if (key === "hashed-password" && !opts?.configFile) {
        throw new Error("--hashed-password can only be set in the config file or passed in via $HASHED_PASSWORD")
S
SPGoding 已提交
324 325
      }

A
Asher 已提交
326 327 328 329 330 331
      const option = options[key]
      if (option.type === "boolean") {
        ;(args[key] as boolean) = true
        continue
      }

332 333 334 335 336 337
      // Might already have a value if it was the --long=value format.
      if (typeof value === "undefined") {
        // A value is only valid if it doesn't look like an option.
        value = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : undefined
      }

A
Asher 已提交
338 339 340 341
      if (!value && option.type === OptionalString) {
        ;(args[key] as OptionalString) = new OptionalString(value)
        continue
      } else if (!value) {
342 343 344
        throw error(`--${key} requires a value`)
      }

A
Asher 已提交
345
      if (option.type === OptionalString && value === "false") {
346
        continue
A
Asher 已提交
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
      }

      if (option.path) {
        value = path.resolve(value)
      }

      switch (option.type) {
        case "string":
          ;(args[key] as string) = value
          break
        case "string[]":
          if (!args[key]) {
            ;(args[key] as string[]) = []
          }
          ;(args[key] as string[]).push(value)
          break
        case "number":
          ;(args[key] as number) = parseInt(value, 10)
          if (isNaN(args[key] as number)) {
366
            throw error(`--${key} must be a number`)
A
Asher 已提交
367 368 369 370 371 372
          }
          break
        case OptionalString:
          ;(args[key] as OptionalString) = new OptionalString(value)
          break
        default: {
373
          if (!Object.values(option.type).includes(value)) {
374
            throw error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
A
Asher 已提交
375 376 377 378 379 380 381 382 383 384 385
          }
          ;(args[key] as string) = value
          break
        }
      }

      continue
    }

    // Everything else goes into _.
    args._.push(arg)
386 387
  }

A
Asher 已提交
388 389 390 391 392
  // If a cert was provided a key must also be provided.
  if (args.cert && args.cert.value && !args["cert-key"]) {
    throw new Error("--cert-key is missing")
  }

A
Asher 已提交
393
  logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })])
A
Asher 已提交
394

A
Asher 已提交
395 396 397
  return args
}

A
Asher 已提交
398 399 400 401 402 403 404 405 406 407
export interface DefaultedArgs extends ConfigArgs {
  auth: AuthType
  cert?: {
    value: string
  }
  host: string
  port: number
  "proxy-domain": string[]
  verbose: boolean
  usingEnvPassword: boolean
S
SPGoding 已提交
408
  usingEnvHashedPassword: boolean
A
Asher 已提交
409 410 411 412 413 414 415 416 417 418 419
  "extensions-dir": string
  "user-data-dir": string
}

/**
 * Take CLI and config arguments (optional) and return a single set of arguments
 * with the defaults set. Arguments from the CLI are prioritized over config
 * arguments.
 */
export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise<DefaultedArgs> {
  const args = Object.assign({}, configArgs || {}, cliArgs)
A
Asher 已提交
420 421 422 423 424 425 426 427 428

  if (!args["user-data-dir"]) {
    args["user-data-dir"] = paths.data
  }

  if (!args["extensions-dir"]) {
    args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
  }

429 430 431 432 433 434 435 436 437
  // --verbose takes priority over --log and --log takes priority over the
  // environment variable.
  if (args.verbose) {
    args.log = LogLevel.Trace
  } else if (
    !args.log &&
    process.env.LOG_LEVEL &&
    Object.values(LogLevel).includes(process.env.LOG_LEVEL as LogLevel)
  ) {
A
Asher 已提交
438
    args.log = process.env.LOG_LEVEL as LogLevel
439
  }
A
Asher 已提交
440

441 442 443 444
  // Sync --log, --verbose, the environment variable, and logger level.
  if (args.log) {
    process.env.LOG_LEVEL = args.log
  }
A
Asher 已提交
445
  switch (args.log) {
A
Asher 已提交
446
    case LogLevel.Trace:
A
Asher 已提交
447
      logger.level = Level.Trace
448
      args.verbose = true
A
Asher 已提交
449
      break
A
Asher 已提交
450
    case LogLevel.Debug:
A
Asher 已提交
451
      logger.level = Level.Debug
452
      args.verbose = false
A
Asher 已提交
453
      break
A
Asher 已提交
454
    case LogLevel.Info:
A
Asher 已提交
455
      logger.level = Level.Info
456
      args.verbose = false
A
Asher 已提交
457
      break
A
Asher 已提交
458
    case LogLevel.Warn:
A
Asher 已提交
459
      logger.level = Level.Warning
460
      args.verbose = false
A
Asher 已提交
461
      break
A
Asher 已提交
462
    case LogLevel.Error:
A
Asher 已提交
463
      logger.level = Level.Error
464
      args.verbose = false
A
Asher 已提交
465 466 467
      break
  }

A
Asher 已提交
468 469 470 471 472
  // Default to using a password.
  if (!args.auth) {
    args.auth = AuthType.Password
  }

A
Asher 已提交
473
  const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs)
474 475
  args.host = addr.host
  args.port = addr.port
A
Asher 已提交
476 477 478 479 480 481 482 483

  // If we're being exposed to the cloud, we listen on a random address and
  // disable auth.
  if (args.link) {
    args.host = "localhost"
    args.port = 0
    args.socket = undefined
    args.cert = undefined
484
    args.auth = AuthType.None
A
Asher 已提交
485 486 487
  }

  if (args.cert && !args.cert.value) {
A
Asher 已提交
488
    const { cert, certKey } = await generateCertificate(args["cert-host"] || "localhost")
A
Asher 已提交
489 490 491 492 493 494
    args.cert = {
      value: cert,
    }
    args["cert-key"] = certKey
  }

S
SPGoding 已提交
495
  let usingEnvPassword = !!process.env.PASSWORD
A
Asher 已提交
496 497 498 499
  if (process.env.PASSWORD) {
    args.password = process.env.PASSWORD
  }

S
SPGoding 已提交
500 501
  const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD
  if (process.env.HASHED_PASSWORD) {
502
    args["hashed-password"] = process.env.HASHED_PASSWORD
S
SPGoding 已提交
503 504 505 506
    usingEnvPassword = false
  }

  // Ensure they're not readable by child processes.
A
Asher 已提交
507
  delete process.env.PASSWORD
S
SPGoding 已提交
508
  delete process.env.HASHED_PASSWORD
A
Asher 已提交
509 510 511 512 513 514 515 516

  // Filter duplicate proxy domains and remove any leading `*.`.
  const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
  args["proxy-domain"] = Array.from(proxyDomains)

  return {
    ...args,
    usingEnvPassword,
S
SPGoding 已提交
517
    usingEnvHashedPassword,
A
Asher 已提交
518
  } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
519 520
}

521 522 523
/**
 * Helper function to return the default config file.
 *
524
 * @param {string} password - Password passed in (usually from generatePassword())
525 526 527 528
 * @returns The default config file:
 *
 * - bind-addr: 127.0.0.1:8080
 * - auth: password
529
 * - password: <password>
530 531
 * - cert: false
 */
532
export function defaultConfigFile(password: string): string {
533
  return `bind-addr: 127.0.0.1:8080
534
auth: password
535
password: ${password}
536 537 538 539
cert: false
`
}

A
Asher 已提交
540 541 542 543
interface ConfigArgs extends Args {
  config: string
}

544 545 546 547 548
/**
 * Reads the code-server yaml config file and returns it as Args.
 *
 * @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default.
 */
A
Asher 已提交
549
export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
550 551 552 553 554 555
  if (!configPath) {
    configPath = process.env.CODE_SERVER_CONFIG
    if (!configPath) {
      configPath = path.join(paths.config, "config.yaml")
    }
  }
A
Anmol Sethi 已提交
556

A
Asher 已提交
557 558 559
  await fs.mkdir(path.dirname(configPath), { recursive: true })

  try {
560 561
    const generatedPassword = await generatePassword()
    await fs.writeFile(configPath, defaultConfigFile(generatedPassword), {
A
Asher 已提交
562 563
      flag: "wx", // wx means to fail if the path exists.
    })
564
    logger.info(`Wrote default config file to ${humanPath(configPath)}`)
565
  } catch (error: any) {
A
Asher 已提交
566 567 568 569
    // EEXIST is fine; we don't want to overwrite existing configurations.
    if (error.code !== "EEXIST") {
      throw error
    }
A
Anmol Sethi 已提交
570 571
  }

A
Asher 已提交
572 573
  const configFile = await fs.readFile(configPath, "utf8")
  return parseConfigFile(configFile, configPath)
574 575 576 577 578 579 580
}

/**
 * parseConfigFile parses configFile into ConfigArgs.
 * configPath is used as the filename in error messages
 */
export function parseConfigFile(configFile: string, configPath: string): ConfigArgs {
A
Anmol Sethi 已提交
581 582
  if (!configFile) {
    return { _: [], config: configPath }
583 584
  }

585
  const config = yaml.load(configFile, {
586
    filename: configPath,
A
Anmol Sethi 已提交
587
  })
A
Anmol Sethi 已提交
588 589 590
  if (!config || typeof config === "string") {
    throw new Error(`invalid config: ${config}`)
  }
A
Anmol Sethi 已提交
591 592 593

  // We convert the config file into a set of flags.
  // This is a temporary measure until we add a proper CLI library.
594
  const configFileArgv = Object.entries(config).map(([optName, opt]) => {
595
    if (opt === true) {
596 597 598 599
      return `--${optName}`
    }
    return `--${optName}=${opt}`
  })
600
  const args = parse(configFileArgv, {
601 602 603 604 605 606 607
    configFile: configPath,
  })
  return {
    ...args,
    config: configPath,
  }
}
A
Anmol Sethi 已提交
608

609
function parseBindAddr(bindAddr: string): Addr {
610
  const u = new URL(`http://${bindAddr}`)
611 612 613 614 615 616 617
  return {
    host: u.hostname,
    // With the http scheme 80 will be dropped so assume it's 80 if missing.
    // This means --bind-addr <addr> without a port will default to 80 as well
    // and not the code-server default.
    port: u.port ? parseInt(u.port, 10) : 80,
  }
A
Anmol Sethi 已提交
618 619
}

620 621 622 623 624
interface Addr {
  host: string
  port: number
}

625 626 627 628 629
/**
 * This function creates the bind address
 * using the CLI args.
 */
export function bindAddrFromArgs(addr: Addr, args: Args): Addr {
630 631
  addr = { ...addr }
  if (args["bind-addr"]) {
632
    addr = parseBindAddr(args["bind-addr"])
A
Anmol Sethi 已提交
633
  }
634 635
  if (args.host) {
    addr.host = args.host
A
Anmol Sethi 已提交
636
  }
637 638 639 640

  if (process.env.PORT) {
    addr.port = parseInt(process.env.PORT, 10)
  }
641 642 643 644 645 646
  if (args.port !== undefined) {
    addr.port = args.port
  }
  return addr
}

647
function bindAddrFromAllSources(...argsConfig: Args[]): Addr {
648 649 650 651 652
  let addr: Addr = {
    host: "localhost",
    port: 8080,
  }

653 654 655
  for (const args of argsConfig) {
    addr = bindAddrFromArgs(addr, args)
  }
656

657
  return addr
A
Anmol Sethi 已提交
658
}
659

A
Asher 已提交
660 661 662 663 664 665 666 667 668 669 670
/**
 * Determine if it looks like the user is trying to open a file or folder in an
 * existing instance. The arguments here should be the arguments the user
 * explicitly passed on the command line, not defaults or the configuration.
 */
export const shouldOpenInExistingInstance = async (args: Args): Promise<string | undefined> => {
  // Always use the existing instance if we're running from VS Code's terminal.
  if (process.env.VSCODE_IPC_HOOK_CLI) {
    return process.env.VSCODE_IPC_HOOK_CLI
  }

A
Asher 已提交
671 672 673
  const readSocketPath = async (): Promise<string | undefined> => {
    try {
      return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
674
    } catch (error: any) {
A
Asher 已提交
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
      if (error.code !== "ENOENT") {
        throw error
      }
    }
    return undefined
  }

  // If these flags are set then assume the user is trying to open in an
  // existing instance since these flags have no effect otherwise.
  const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
    return args[cur as keyof Args] ? prev + 1 : prev
  }, 0)
  if (openInFlagCount > 0) {
    return readSocketPath()
  }

  // It's possible the user is trying to spawn another instance of code-server.
A
Asher 已提交
692 693 694 695
  // Check if any unrelated flags are set (check against one because `_` always
  // exists), that a file or directory was passed, and that the socket is
  // active.
  if (Object.keys(args).length === 1 && args._.length > 0) {
A
Asher 已提交
696 697 698 699 700
    const socketPath = await readSocketPath()
    if (socketPath && (await canConnect(socketPath))) {
      return socketPath
    }
  }
A
Asher 已提交
701 702 703

  return undefined
}