diff --git a/scripts/vscode.patch b/scripts/vscode.patch index afc7c10f..a40b695b 100644 --- a/scripts/vscode.patch +++ b/scripts/vscode.patch @@ -852,7 +852,7 @@ index 0000000000..eb62b87798 +} diff --git a/src/vs/server/entry.ts b/src/vs/server/entry.ts new file mode 100644 -index 0000000000..cb606e6a68 +index 0000000000..9995e9f7fc --- /dev/null +++ b/src/vs/server/entry.ts @@ -0,0 +1,67 @@ @@ -897,8 +897,8 @@ index 0000000000..cb606e6a68 + process.send(message); +}; + -+// Wait for the init message then start up VS Code. Future messages will return -+// new workbench options without starting a new instance. ++// Wait for the init message then start up VS Code. Subsequent messages will ++// return new workbench options without starting a new instance. +process.on('message', async (message: CodeServerMessage, socket) => { + logger.debug('got message from code-server', field('message', message)); + switch (message.type) { @@ -934,10 +934,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..218faa34d2 +index 0000000000..3bfef75d81 --- /dev/null +++ b/src/vs/server/ipc.d.ts -@@ -0,0 +1,93 @@ +@@ -0,0 +1,91 @@ +/** + * External interfaces for integration into code-server over IPC. No vs imports + * should be made in this file. @@ -971,19 +971,18 @@ index 0000000000..218faa34d2 +export type VscodeMessage = ReadyMessage | OptionsMessage; + +export interface StartPath { -+ path?: string[] | string; -+ workspace?: boolean; ++ url: string; ++ workspace: boolean; +} + -+export interface Settings { -+ lastVisited?: StartPath; ++export interface Args { ++ _: string[]; +} + +export interface VscodeOptions { + readonly remoteAuthority: string; -+ readonly query: Query; -+ readonly args?: string[]; -+ readonly settings: Settings; ++ readonly args: Args; ++ readonly startPath?: StartPath; +} + +export interface VscodeOptionsMessage extends VscodeOptions { @@ -1008,7 +1007,6 @@ index 0000000000..218faa34d2 +} + +export interface WorkbenchOptions { -+ readonly startPath?: StartPath; + readonly workbenchWebConfiguration: { + readonly remoteAuthority?: string; + readonly folderUri?: UriComponents; @@ -2181,15 +2179,13 @@ 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..5207c90081 +index 0000000000..81d275a80a --- /dev/null +++ b/src/vs/server/node/server.ts -@@ -0,0 +1,293 @@ -+import * as fs from 'fs-extra'; +@@ -0,0 +1,253 @@ +import * as net from 'net'; +import * as path from 'path'; +import { Emitter } from 'vs/base/common/event'; -+import { sanitizeFilePath } from 'vs/base/common/extpath'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { getMachineId } from 'vs/base/node/id'; @@ -2232,12 +2228,10 @@ index 0000000000..5207c90081 +import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; +import { INodeProxyService, NodeProxyChannel } from 'vs/server/common/nodeProxy'; +import { TelemetryChannel } from 'vs/server/common/telemetry'; -+import { Query, StartPath, VscodeOptions, WorkbenchOptions } from 'vs/server/ipc'; ++import { Query, VscodeOptions, WorkbenchOptions } from 'vs/server/ipc'; +import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from 'vs/server/node/channel'; -+import { parseArgs } from 'vs/server/node/cli'; +import { Connection, ExtensionHostConnection, ManagementConnection } from 'vs/server/node/connection'; +import { TelemetryClient } from 'vs/server/node/insights'; -+import { logger } from 'vs/server/node/logger'; +import { getLocaleFromConfig, getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; +import { getUriTransformer } from 'vs/server/node/util'; @@ -2253,27 +2247,19 @@ index 0000000000..5207c90081 + + private readonly services = new ServiceCollection(); + private servicesPromise?: Promise; -+ private args?: ParsedArgs; + + public async initialize(options: VscodeOptions): Promise { -+ if (!this.args) { -+ this.args = parseArgs(options.args || []); -+ } + const transformer = getUriTransformer(options.remoteAuthority); -+ const startPath = await this.getFirstValidPath([ -+ options.settings.lastVisited, -+ { path: this.args._[0] }, -+ ]); + if (!this.servicesPromise) { -+ this.servicesPromise = this.initializeServices(this.args); ++ this.servicesPromise = this.initializeServices(options.args); + } + await this.servicesPromise; + const environment = this.services.get(IEnvironmentService) as IEnvironmentService; ++ const startPath = options.startPath; + return { -+ startPath, + workbenchWebConfiguration: { -+ workspaceUri: startPath && startPath.workspace ? transformer.transformOutgoing(URI.file(startPath.path)) : undefined, -+ folderUri: startPath && !startPath.workspace ? transformer.transformOutgoing(URI.file(startPath.path)) : undefined, ++ workspaceUri: startPath && startPath.workspace ? URI.parse(startPath.url) : undefined, ++ folderUri: startPath && !startPath.workspace ? URI.parse(startPath.url) : undefined, + remoteAuthority: options.remoteAuthority, + logLevel: getLogLevel(environment), + }, @@ -2305,34 +2291,6 @@ index 0000000000..5207c90081 + return true; + } + -+ /** -+ * Choose the first valid path. If `workspace` is undefined then either a -+ * workspace or a directory are acceptable. Otherwise it must be a file if a -+ * workspace or a directory otherwise. -+ */ -+ private async getFirstValidPath(startPaths: Array): Promise<{ path: string, workspace?: boolean} | undefined> { -+ const cwd = process.env.VSCODE_CWD || process.cwd(); -+ for (let i = 0; i < startPaths.length; ++i) { -+ const startPath = startPaths[i]; -+ if (!startPath) { -+ continue; -+ } -+ const paths = typeof startPath.path === 'string' ? [startPath.path] : (startPath.path || []); -+ for (let j = 0; j < paths.length; ++j) { -+ const p = sanitizeFilePath(paths[j], cwd); -+ try { -+ const stat = await fs.stat(p); -+ if (typeof startPath.workspace === 'undefined' || startPath.workspace !== stat.isDirectory()) { -+ return { path: p, workspace: !stat.isDirectory() }; -+ } -+ } catch (error) { -+ logger.warn(error.message); -+ } -+ } -+ } -+ return undefined; -+ } -+ + private async connect(message: ConnectionTypeRequest, protocol: Protocol): Promise { + if (product.commit && message.commit !== product.commit) { + throw new Error(`Version mismatch (${message.commit} instead of ${product.commit})`); diff --git a/src/node/cli.ts b/src/node/cli.ts new file mode 100644 index 00000000..5ac5786d --- /dev/null +++ b/src/node/cli.ts @@ -0,0 +1,27 @@ +import { AuthType } from "./http" +import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" + +export interface Args extends VsArgs { + auth?: AuthType + "base-path"?: string + cert?: string + "cert-key"?: string + format?: string + host?: string + json?: boolean + open?: boolean + port?: string + socket?: string + version?: boolean + _: string[] +} + +// TODO: Implement proper CLI parser. +export const parse = (): Args => { + const last = process.argv[process.argv.length - 1] + return { + version: process.argv.includes("--version"), + json: process.argv.includes("--json"), + _: last && !last.startsWith("-") ? [last] : [], + } +} diff --git a/src/node/entry.ts b/src/node/entry.ts index 2187b396..4a10155f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -1,25 +1,13 @@ import { logger } from "@coder/logger" import { ApiHttpProvider } from "./api/server" import { MainHttpProvider } from "./app/server" +import { Args, parse } from "./cli" import { AuthType, HttpServer } from "./http" import { generateCertificate, generatePassword, hash, open } from "./util" import { VscodeHttpProvider } from "./vscode/server" import { ipcMain, wrap } from "./wrapper" -export interface Args { - auth?: AuthType - "base-path"?: string - cert?: string - "cert-key"?: string - format?: string - host?: string - open?: boolean - port?: string - socket?: string - _?: string[] -} - -const main = async (args: Args = {}): Promise => { +const main = async (args: Args): Promise => { const auth = args.auth || AuthType.Password const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) @@ -51,7 +39,7 @@ const main = async (args: Args = {}): Promise => { const httpServer = new HttpServer(options) httpServer.registerHttpProvider("/", MainHttpProvider) httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer) - httpServer.registerHttpProvider("/vscode-embed", VscodeHttpProvider, []) + httpServer.registerHttpProvider("/vscode-embed", VscodeHttpProvider, args) ipcMain().onDispose(() => httpServer.dispose()) @@ -88,10 +76,10 @@ const main = async (args: Args = {}): Promise => { } } -// TODO: Implement CLI parser. -if (process.argv.includes("--version")) { +const args = parse() +if (args.version) { const version = require("../../package.json").version - if (process.argv.includes("--json")) { + if (args.json) { console.log({ codeServer: version, vscode: require("../../lib/vscode/package.json").version, @@ -101,5 +89,5 @@ if (process.argv.includes("--version")) { } process.exit(0) } else { - wrap(main) + wrap(() => main(args)) } diff --git a/src/node/vscode/error.html b/src/node/vscode/error.html index 48cd03aa..dc3ef0bb 100644 --- a/src/node/vscode/error.html +++ b/src/node/vscode/error.html @@ -6,7 +6,7 @@ code-server - +
{{ERROR}}
diff --git a/src/node/vscode/server.ts b/src/node/vscode/server.ts index de984ab5..86eaf8a1 100644 --- a/src/node/vscode/server.ts +++ b/src/node/vscode/server.ts @@ -1,22 +1,29 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" import * as crypto from "crypto" +import * as fs from "fs-extra" import * as http from "http" import * as net from "net" import * as path from "path" +import * as url from "url" import { CodeServerMessage, - Settings, + StartPath, VscodeMessage, VscodeOptions, WorkbenchOptions, } from "../../../lib/vscode/src/vs/server/ipc" import { HttpCode, HttpError } from "../../common/http" import { generateUuid } from "../../common/util" +import { Args } from "../cli" import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { SettingsProvider } from "../settings" import { xdgLocalDir } from "../util" +export interface Settings { + lastVisited: StartPath +} + export class VscodeHttpProvider extends HttpProvider { private readonly serverRootPath: string private readonly vsRootPath: string @@ -24,7 +31,7 @@ export class VscodeHttpProvider extends HttpProvider { private _vscode?: Promise private workbenchOptions?: WorkbenchOptions - public constructor(options: HttpProviderOptions, private readonly args: string[]) { + public constructor(options: HttpProviderOptions, private readonly args: Args) { super(options) this.vsRootPath = path.resolve(this.rootPath, "lib/vscode") this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") @@ -161,24 +168,26 @@ export class VscodeHttpProvider extends HttpProvider { private async getRoot(request: http.IncomingMessage, route: Route): Promise { const settings = await this.settings.read() + const startPath = await this.getFirstValidPath([ + { url: route.query.workspace, workspace: true }, + { url: route.query.folder, workspace: false }, + settings.lastVisited, + this.args._ && this.args._.length > 0 ? { url: this.urlify(this.args._[0]) } : undefined, + ]) const [response, options] = await Promise.all([ await this.getUtf8Resource(this.rootPath, `src/node/vscode/workbench${!this.isDev ? "-build" : ""}.html`), this.initialize({ args: this.args, - query: route.query, remoteAuthority: request.headers.host as string, - settings, + startPath, }), ]) this.workbenchOptions = options - if (options.startPath) { + if (startPath) { this.settings.write({ - lastVisited: { - path: options.startPath.path, - workspace: options.startPath.workspace, - }, + lastVisited: startPath, }) } @@ -201,4 +210,40 @@ export class VscodeHttpProvider extends HttpProvider { response.content = response.content.replace(/{{COMMIT}}/g, this.options.commit).replace(/{{ERROR}}/g, message) return response } + + /** + * Choose the first valid path. If `workspace` is undefined then either a + * workspace or a directory are acceptable. Otherwise it must be a file if a + * workspace or a directory otherwise. + */ + private async getFirstValidPath( + startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined> + ): Promise { + for (let i = 0; i < startPaths.length; ++i) { + const startPath = startPaths[i] + if (!startPath) { + continue + } + const paths = typeof startPath.url === "string" ? [startPath.url] : startPath.url || [] + for (let j = 0; j < paths.length; ++j) { + const u = url.parse(paths[j]) + try { + if (!u.pathname) { + throw new Error(`${paths[j]} is not a valid URL`) + } + const stat = await fs.stat(u.pathname) + if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) { + return { url: u.href, workspace: !stat.isDirectory() } + } + } catch (error) { + logger.warn(error.message) + } + } + } + return undefined + } + + private urlify(p: string): string { + return "vscode-remote://host" + path.resolve(p) + } }