diff --git a/ci/vscode.patch b/ci/vscode.patch index 77fe4367a..8590fae54 100644 --- a/ci/vscode.patch +++ b/ci/vscode.patch @@ -12,7 +12,7 @@ index 160c42ed74..0d544c495c 100644 coverage/ diff --git a/coder.js b/coder.js new file mode 100644 -index 0000000000..fc18355f89 +index 0000000000..6aee0e46bc --- /dev/null +++ b/coder.js @@ -0,0 +1,70 @@ @@ -32,9 +32,9 @@ index 0000000000..fc18355f89 + buildfile.workbenchWeb, + buildfile.workerExtensionHost, + buildfile.keyboardMaps, -+ buildfile.entrypoint('vs/platform/files/node/watcher/unix/watcherApp', ["vs/css", "vs/nls"]), -+ buildfile.entrypoint('vs/platform/files/node/watcher/nsfw/watcherApp', ["vs/css", "vs/nls"]), -+ buildfile.entrypoint('vs/workbench/services/extensions/node/extensionHostProcess', ["vs/css", "vs/nls"]), ++ buildfile.entrypoint("vs/platform/files/node/watcher/unix/watcherApp", ["vs/css", "vs/nls"]), ++ buildfile.entrypoint("vs/platform/files/node/watcher/nsfw/watcherApp", ["vs/css", "vs/nls"]), ++ buildfile.entrypoint("vs/workbench/services/extensions/node/extensionHostProcess", ["vs/css", "vs/nls"]), +]); + +const vscodeResources = [ @@ -894,10 +894,10 @@ index 0000000000..eb62b87798 +} diff --git a/src/vs/server/entry.ts b/src/vs/server/entry.ts new file mode 100644 -index 0000000000..9995e9f7fc +index 0000000000..0d7feaa24e --- /dev/null +++ b/src/vs/server/entry.ts -@@ -0,0 +1,67 @@ +@@ -0,0 +1,76 @@ +import { field } from '@coder/logger'; +import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { CodeServerMessage, VscodeMessage } from 'vs/server/ipc'; @@ -953,6 +953,15 @@ index 0000000000..9995e9f7fc + exit(1); + } + break; ++ case 'cli': ++ try { ++ await vscode.cli(message.args); ++ exit(0); ++ } catch (error) { ++ logger.error(error.message); ++ exit(1); ++ } ++ break; + case 'socket': + vscode.handleWebSocket(socket, message.query); + break; @@ -976,10 +985,10 @@ index 0000000000..56331ff1fc +require('../../bootstrap-amd').load('vs/server/entry'); diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts new file mode 100644 -index 0000000000..a1047fff86 +index 0000000000..82566066ff --- /dev/null +++ b/src/vs/server/ipc.d.ts -@@ -0,0 +1,101 @@ +@@ -0,0 +1,106 @@ +/** + * External interfaces for integration into code-server over IPC. No vs imports + * should be made in this file. @@ -998,7 +1007,12 @@ index 0000000000..a1047fff86 + query: Query; +} + -+export type CodeServerMessage = InitMessage | SocketMessage; ++export interface CliMessage { ++ type: 'cli'; ++ args: Args; ++} ++ ++export type CodeServerMessage = InitMessage | SocketMessage | CliMessage; + +export interface ReadyMessage { + type: 'ready'; @@ -1032,8 +1046,8 @@ index 0000000000..a1047fff86 +} + +export interface VscodeOptions { -+ readonly remoteAuthority: string; + readonly args: Args; ++ readonly remoteAuthority: string; + readonly startPath?: StartPath; +} + @@ -2152,10 +2166,10 @@ index 0000000000..3c74512192 +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 0000000000..13d71949ce +index 0000000000..20dbca69b2 --- /dev/null +++ b/src/vs/server/node/server.ts -@@ -0,0 +1,252 @@ +@@ -0,0 +1,257 @@ +import * as net from 'net'; +import * as path from 'path'; +import { Emitter } from 'vs/base/common/event'; @@ -2165,6 +2179,7 @@ index 0000000000..13d71949ce +import { ClientConnectionEvent, IPCServer, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { createChannelReceiver } from 'vs/base/parts/ipc/node/ipc'; +import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; ++import { main } from "vs/code/node/cliProcessMain"; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; @@ -2222,6 +2237,10 @@ index 0000000000..13d71949ce + private readonly services = new ServiceCollection(); + private servicesPromise?: Promise; + ++ public async cli(args: ParsedArgs): Promise { ++ return main(args); ++ } ++ + public async initialize(options: VscodeOptions): Promise { + const transformer = getUriTransformer(options.remoteAuthority); + if (!this.servicesPromise) { diff --git a/src/browser/media/manifest.json b/src/browser/media/manifest.json index c18799260..8b2a7497c 100644 --- a/src/browser/media/manifest.json +++ b/src/browser/media/manifest.json @@ -1,13 +1,13 @@ { "name": "code-server", "short_name": "code-server", - "start_url": "../../../..", + "start_url": "{{BASE}}", "display": "fullscreen", "background-color": "#fff", "description": "Run editors on a remote server.", "icons": [ { - "src": "./code-server.png", + "src": "{{BASE}}/static-{{COMMIT}}/src/browser/media/code-server.png", "sizes": "384x384", "type": "image/png" } diff --git a/src/node/app/app.ts b/src/node/app/app.ts index 26653605e..298ba9744 100644 --- a/src/node/app/app.ts +++ b/src/node/app/app.ts @@ -24,7 +24,7 @@ export class MainHttpProvider extends HttpProvider { switch (route.base) { case "/static": { this.ensureMethod(request) - const response = await this.getResource(this.rootPath, route.requestPath) + const response = await this.getReplacedResource(route) if (!this.isDev) { response.cache = true } @@ -75,6 +75,20 @@ export class MainHttpProvider extends HttpProvider { return this.getErrorRoot(route, "404", "404", "Application not found") } + /** + * Return a resource with variables replaced where necessary. + */ + protected async getReplacedResource(route: Route): Promise { + if (route.requestPath.endsWith("/manifest.json")) { + const response = await this.getUtf8Resource(this.rootPath, route.requestPath) + response.content = response.content + .replace(/{{BASE}}/g, this.base(route)) + .replace(/{{COMMIT}}/g, this.options.commit) + return response + } + return this.getResource(this.rootPath, route.requestPath) + } + public async getRoot(route: Route): Promise { const recent = await this.api.recent() const apps = await this.api.installedApplications() @@ -136,7 +150,7 @@ export class MainHttpProvider extends HttpProvider { private async getUpdate(): Promise { if (!this.update.enabled) { - return "Updates are disabled" + return `
Updates are disabled
` } const humanize = (time: number): string => { diff --git a/src/node/cli.ts b/src/node/cli.ts index 63ac29c60..4389c8765 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -15,6 +15,7 @@ export interface Args extends VsArgs { readonly cert?: OptionalString readonly "cert-key"?: string readonly "disable-updates"?: boolean + readonly "disable-telemetry"?: boolean readonly help?: boolean readonly host?: string readonly json?: boolean @@ -22,6 +23,9 @@ export interface Args extends VsArgs { readonly port?: number readonly socket?: string readonly version?: boolean + readonly "list-extensions"?: boolean + readonly "install-extension"?: string[] + readonly "uninstall-extension"?: string[] readonly _: string[] } @@ -68,6 +72,7 @@ const options: Options> = { }, "cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." }, "disable-updates": { type: "boolean", description: "Disable automatic updates." }, + "disable-telemetry": { type: "boolean", description: "Disable telemetry." }, host: { type: "string", description: "Host for the HTTP server." }, help: { type: "boolean", short: "h", description: "Show this output." }, json: { type: "boolean" }, @@ -82,6 +87,9 @@ const options: Options> = { "builtin-extensions-dir": { type: "string", path: true }, "extra-extensions-dir": { type: "string[]", path: true }, "extra-builtin-extensions-dir": { type: "string[]", path: true }, + "list-extensions": { type: "boolean" }, + "install-extension": { type: "string[]" }, + "uninstall-extension": { type: "string[]" }, log: { type: "string" }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, @@ -193,8 +201,13 @@ export const parse = (argv: string[]): Args => { if (process.env.LOG_LEVEL === "trace" || args.verbose) { args.verbose = true args.log = "trace" + } else if (!args.log) { + args.log = process.env.LOG_LEVEL } + // Ensure this passes down to forked processes. + process.env.LOG_LEVEL = args.log + switch (args.log) { case "trace": logger.level = Level.Trace diff --git a/src/node/entry.ts b/src/node/entry.ts index caa8bd94c..3d8df7d14 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -1,10 +1,13 @@ -import { logger } from "@coder/logger" -import { Args, optionDescriptions, parse } from "./cli" +import { field, logger } from "@coder/logger" +import * as cp from "child_process" +import * as path from "path" +import { CliMessage } from "../../lib/vscode/src/vs/server/ipc" import { ApiHttpProvider } from "./app/api" import { MainHttpProvider } from "./app/app" import { LoginHttpProvider } from "./app/login" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" +import { Args, optionDescriptions, parse } from "./cli" import { AuthType, HttpServer } from "./http" import { generateCertificate, generatePassword, hash, open } from "./util" import { ipcMain, wrap } from "./wrapper" @@ -105,6 +108,29 @@ if (args.help) { console.log(version) } process.exit(0) +} else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) { + process.env.NBIN_BYPASS = "true" + logger.debug("Forking VS Code CLI...") + const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { + env: { + ...process.env, + CODE_SERVER_PARENT_PID: process.pid.toString(), + }, + }) + vscode.once("message", (message) => { + logger.debug("Got message from VS Code", field("message", message)) + if (message.type !== "ready") { + logger.error("Unexpected response waiting for ready response") + process.exit(1) + } + const send: CliMessage = { type: "cli", args } + vscode.send(send) + }) + vscode.once("error", (error) => { + logger.error(error.message) + process.exit(1) + }) + vscode.on("exit", (code) => process.exit(code || 0)) } else { wrap(() => main(args)) }