import * as cp from "child_process"; import * as os from "os"; import { main as vsCli } from "vs/code/node/cliProcessMain"; import { validatePaths } from "vs/code/node/paths"; import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper"; import { buildHelpMessage, buildVersionMessage, options } from "vs/platform/environment/node/argv"; import { ParsedArgs } from "vs/platform/environment/common/environment"; import pkg from "vs/platform/product/node/package"; import product from "vs/platform/product/node/product"; import { ipcMain } from "vs/server/src/ipc"; product.extensionsGallery = { serviceUrl: process.env.SERVICE_URL || "https://v1.extapi.coder.com", itemUrl: process.env.ITEM_URL || "", controlUrl: "", recommendationsUrl: "", ...(product.extensionsGallery || {}), }; import { MainServer } from "vs/server/src/server"; import { enableExtensionTars } from "vs/server/src/tar"; import { AuthType, buildAllowedMessage, generateCertificate, generatePassword, localRequire, open, unpackExecutables } from "vs/server/src/util"; const { logger } = localRequire("@coder/logger/out/index"); interface Args extends ParsedArgs { auth?: AuthType; "base-path"?: string; cert?: string; "cert-key"?: string; "extra-builtin-extensions-dir"?: string; "extra-extensions-dir"?: string; host?: string; open?: string; port?: string; socket?: string; } const getArgs = (): Args => { // The last item is _ which is like -- so our options need to come before it. const last = options.pop()!; // Remove options that won't work or don't make sense. let i = options.length; while (i--) { switch (options[i].id) { case "add": case "diff": case "file-uri": case "folder-uri": case "goto": case "new-window": case "reuse-window": case "wait": case "disable-gpu": // TODO: pretty sure these don't work but not 100%. case "max-memory": case "prof-startup": case "inspect-extensions": case "inspect-brk-extensions": options.splice(i, 1); break; } } options.push({ id: "base-path", type: "string", cat: "o", description: "Base path of the URL at which code-server is hosted (used for login redirects)." }); options.push({ id: "cert", type: "string", cat: "o", description: "Path to certificate. If the path is omitted, both this and --cert-key will be generated." }); options.push({ id: "cert-key", type: "string", cat: "o", description: "Path to the certificate's key if one was provided." }); options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to an extra builtin extension directory." }); options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to an extra user extension directory." }); options.push({ id: "host", type: "string", cat: "o", description: "Host for the server." }); options.push({ id: "auth", type: "string", cat: "o", description: `The type of authentication to use. ${buildAllowedMessage(AuthType)}.` }); options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." }); options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." }); options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." }); options.push(last); const args = validatePaths(parseMainProcessArgv(process.argv)) as Args; ["extra-extensions-dir", "extra-builtin-extensions-dir"].forEach((key) => { if (typeof args[key] === "string") { args[key] = [args[key]]; } }); return args; }; const startVscode = async (): Promise => { const args = getArgs(); const extra = args["_"] || []; const options = { auth: args.auth, basePath: args["base-path"], cert: args.cert, certKey: args["cert-key"], folderUri: extra.length > 1 ? extra[extra.length - 1] : undefined, host: args.host, password: process.env.PASSWORD, }; if (options.auth && Object.keys(AuthType).filter((k) => AuthType[k] === options.auth).length === 0) { throw new Error(`'${options.auth}' is not a valid authentication type.`); } else if (options.auth && !options.password) { options.password = await generatePassword(); } if (!options.certKey && typeof options.certKey !== "undefined") { throw new Error(`--cert-key cannot be blank`); } else if (options.certKey && !options.cert) { throw new Error(`--cert-key was provided but --cert was not`); } if (!options.cert && typeof options.cert !== "undefined") { const { cert, certKey } = await generateCertificate(); options.cert = cert; options.certKey = certKey; } enableExtensionTars(); const server = new MainServer({ ...options, port: typeof args.port !== "undefined" && parseInt(args.port, 10) || 8443, socket: args.socket, }, args); const [serverAddress, /* ignore */] = await Promise.all([ server.listen(), unpackExecutables(), ]); logger.info(`Server listening on ${serverAddress}`); if (options.auth && !process.env.PASSWORD) { logger.info(` - Password is ${options.password}`); logger.info(" - To use your own password, set the PASSWORD environment variable"); } else if (options.auth) { logger.info(" - Using custom password for authentication"); } else { logger.info(" - No authentication"); } if (server.protocol === "https") { logger.info( args.cert ? ` - Using provided certificate${args["cert-key"] ? " and key" : ""} for HTTPS` : ` - Using generated certificate and key for HTTPS`, ); } else { logger.info(" - Not serving HTTPS"); } if (!server.options.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = `http://localhost:${server.options.port}`; await open(openAddress).catch(console.error); logger.info(` - Opened ${openAddress}`); } }; const startCli = (): boolean | Promise => { const args = getArgs(); if (args.help) { const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`; console.log(buildHelpMessage(product.nameLong, executable, pkg.codeServerVersion, undefined, false)); return true; } if (args.version) { buildVersionMessage(pkg.codeServerVersion, product.commit).split("\n").map((line) => logger.info(line)); return true; } const shouldSpawnCliProcess = (): boolean => { return !!args["install-source"] || !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"] || !!args["locate-extension"] || !!args["telemetry"]; }; if (shouldSpawnCliProcess()) { enableExtensionTars(); return vsCli(args); } return false; }; export class WrapperProcess { private process?: cp.ChildProcess; private started?: Promise; public constructor() { ipcMain.onMessage(async (message) => { switch (message) { case "relaunch": logger.info("Relaunching..."); this.started = undefined; if (this.process) { this.process.kill(); } try { await this.start(); } catch (error) { logger.error(error.message); process.exit(typeof error.code === "number" ? error.code : 1); } break; default: logger.error(`Unrecognized message ${message}`); break; } }); } public start(): Promise { if (!this.started) { const child = this.spawn(); this.started = ipcMain.handshake(child); this.process = child; } return this.started; } private spawn(): cp.ChildProcess { return cp.spawn(process.argv[0], process.argv.slice(1), { env: { ...process.env, LAUNCH_VSCODE: "true", }, stdio: ["inherit", "inherit", "inherit", "ipc"], }); } } const main = async(): Promise => { if (process.env.LAUNCH_VSCODE) { await ipcMain.handshake(); return startVscode(); } return startCli() || new WrapperProcess().start(); }; main().catch((error) => { logger.error(error.message); process.exit(typeof error.code === "number" ? error.code : 1); });