diff --git a/.gitignore b/.gitignore index b7f5b58c8ede171be547c56b61ce76f79a3accc3..856fbd8c67460fe099d7fbee1475e906b500f053 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ out-vscode-reh-web-pkg/ out-vscode-web/ out-vscode-web-min/ out-vscode-web-pkg/ -src/vs/server resources/server build/node_modules coverage/ diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index d97527dab46aa4e7aa2df386bda3a8b4f93fcb80..0000000000000000000000000000000000000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "https://electronjs.org/headers" -target "9.3.3" -runtime "electron" diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 5f367d1f0777d2cb46ad47e376337900733981b5..ba74af1d61a00ce42020418126e62879397f57bf 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -44,6 +44,7 @@ BUILD_TARGETS.forEach(({ platform, arch }) => { }); function getNodeVersion() { + return process.versions.node; const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index dac71c814798ecfac99750be856078e043d239bf..6edd7ea56baef7cd9f87a9020df32d3b8519b615 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -70,7 +70,7 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { if (isWebPacked) { input = updateExtensionPackageJSON(input, (data: any) => { delete data.scripts; - delete data.dependencies; + // https://github.com/cdr/code-server/pull/2041#issuecomment-685910322 delete data.devDependencies; if (data.main) { data.main = data.main.replace('/out/', /dist/); diff --git a/build/lib/node.ts b/build/lib/node.ts index 64397034461b1661f82007c141cbf4c039a3b722..c53dccf4dc0a99122ed96cf10c2eb632bb25059e 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -4,13 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import * as fs from 'fs'; const root = path.dirname(path.dirname(__dirname)); -const yarnrcPath = path.join(root, 'remote', '.yarnrc'); -const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); -const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)![1]; +const version = process.versions.node; const node = process.platform === 'win32' ? 'node.exe' : 'node'; const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node); -console.log(nodePath); \ No newline at end of file +console.log(nodePath); diff --git a/build/lib/util.ts b/build/lib/util.ts index c0a0d9619d736c6558b0b91e6c7537c1a06cc947..48853bc6201a602cadbef47a8f46281be93421e9 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -336,6 +336,7 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { } export function getElectronVersion(): string { + return process.versions.node; const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)![1]; return target; diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 8f8b0019a7792a993fbd6bf95b013b596aa2935a..ea054c725bea2eec342e12b07314241aa18a4951 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -33,10 +33,11 @@ function yarnInstall(location, opts) { yarnInstall('extensions'); // node modules shared by all extensions -if (!(process.platform === 'win32' && (process.arch === 'arm64' || process.env['npm_config_arch'] === 'arm64'))) { - yarnInstall('remote'); // node modules used by vscode server - yarnInstall('remote/web'); // node modules used by vscode web -} +// NOTE@coder: Skip these dependencies since we don't use them. +// if (!(process.platform === 'win32' && (process.arch === 'arm64' || process.env['npm_config_arch'] === 'arm64'))) { +// yarnInstall('remote'); // node modules used by vscode server +// yarnInstall('remote/web'); // node modules used by vscode web +// } const allExtensionFolders = fs.readdirSync('extensions'); const extensions = allExtensionFolders.filter(e => { @@ -69,9 +70,9 @@ runtime "${runtime}"`; } yarnInstall(`build`); // node modules required for build -yarnInstall('test/automation'); // node modules required for smoketest -yarnInstall('test/smoke'); // node modules required for smoketest -yarnInstall('test/integration/browser'); // node modules required for integration +// yarnInstall('test/automation'); // node modules required for smoketest +// yarnInstall('test/smoke'); // node modules required for smoketest +// yarnInstall('test/integration/browser'); // node modules required for integration yarnInstallBuildDependencies(); // node modules for watching, specific to host node version, not electron cp.execSync('git config pull.rebase true'); diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index cb88d37adefd4882f61a2711fdd7f72b89e1a6e3..6b3253af0a3a0aa4d75456379ef1c00f4cb98d13 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -8,8 +8,9 @@ let err = false; const majorNodeVersion = parseInt(/^(\d+)\./.exec(process.versions.node)[1]); if (majorNodeVersion < 10 || majorNodeVersion >= 13) { - console.error('\033[1;31m*** Please use node >=10 and <=12.\033[0;0m'); - err = true; + // We are ok building above Node 12. + // console.error('\033[1;31m*** Please use node >=10 and <=12.\033[0;0m'); + // err = true; } const cp = require('child_process'); diff --git a/coder.js b/coder.js new file mode 100644 index 0000000000000000000000000000000000000000..df5b42cba463b6c0043aebbc835f852f1284aa36 --- /dev/null +++ b/coder.js @@ -0,0 +1,64 @@ +// This must be ran from VS Code's root. +const gulp = require("gulp"); +const path = require("path"); +const _ = require("underscore"); +const buildfile = require("./src/buildfile"); +const common = require("./build/lib/optimize"); +const util = require("./build/lib/util"); +const deps = require("./build/dependencies"); + +const vscodeEntryPoints = _.flatten([ + buildfile.entrypoint("vs/workbench/workbench.web.api"), + buildfile.entrypoint("vs/server/entry"), + buildfile.base, + buildfile.workbenchWeb, + buildfile.workerExtensionHost, + buildfile.workerNotebook, + 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"]), +]); + +const vscodeResources = [ + "out-build/vs/server/fork.js", + "!out-build/vs/server/doc/**", + "out-build/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js", + "out-build/bootstrap.js", + "out-build/bootstrap-fork.js", + "out-build/bootstrap-amd.js", + 'out-build/bootstrap-node.js', + "out-build/paths.js", + 'out-build/vs/**/*.{svg,png,html,ttf}', + "!out-build/vs/code/browser/workbench/*.html", + '!out-build/vs/code/electron-browser/**', + "out-build/vs/base/common/performance.js", + "out-build/vs/base/node/languagePacks.js", + 'out-build/vs/base/browser/ui/codicons/codicon/**', + "out-build/vs/workbench/browser/media/*-theme.css", + "out-build/vs/workbench/contrib/debug/**/*.json", + "out-build/vs/workbench/contrib/externalTerminal/**/*.scpt", + "out-build/vs/workbench/contrib/webview/browser/pre/*.js", + "out-build/vs/**/markdown.css", + "out-build/vs/workbench/contrib/tasks/**/*.json", + "out-build/vs/platform/files/**/*.md", + "!**/test/**" +]; + +gulp.task("optimize", gulp.series( + util.rimraf("out-vscode"), + common.optimizeTask({ + src: "out-build", + entryPoints: vscodeEntryPoints, + resources: vscodeResources, + loaderConfig: common.loaderConfig(), + out: "out-vscode", + inlineAmdImages: true, + bundleInfo: undefined + }), +)); + +gulp.task("minify", gulp.series( + util.rimraf("out-vscode-min"), + common.minifyTask("out-vscode") +)); diff --git a/extensions/postinstall.js b/extensions/postinstall.js index da4fa3e9d0443d679dfbab1000b434af2ae01afd..50f3e1144f8057883dea8b91ec2f7073458dbd94 100644 --- a/extensions/postinstall.js +++ b/extensions/postinstall.js @@ -24,6 +24,9 @@ function processRoot() { rimraf.sync(filePath); } } + + // Delete .bin so it doesn't contain broken symlinks that trip up nfpm. + rimraf.sync(path.join(__dirname, 'node_modules', '.bin')); } function processLib() { diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts index 2d754bf4054713f53beed030f9211b33532c1b4b..708b7e40a662e4ca93420992bf7a5af0c62ea5b2 100644 --- a/extensions/typescript-language-features/src/utils/platform.ts +++ b/extensions/typescript-language-features/src/utils/platform.ts @@ -6,6 +6,6 @@ import * as vscode from 'vscode'; export function isWeb(): boolean { - // @ts-expect-error + // NOTE@coder: Remove unused ts-expect-error directive which causes tsc to error. return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; } diff --git a/package.json b/package.json index 28f8a69a2a91f9cb9f4dbd73ed3e689b2b3afe84..b5f5b10004d3e36092a30f685938a606b333c465 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,11 @@ "watch-web": "gulp watch-web --max_old_space_size=4095", "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions" }, + "dependencies_comment": "Move rimraf to dependencies because it is used in the postinstall script.", "dependencies": { + "@coder/logger": "^1.1.12", + "@coder/node-browser": "^1.0.8", + "@coder/requirefs": "^1.1.5", "applicationinsights": "1.0.8", "chokidar": "3.4.3", "graceful-fs": "4.2.3", @@ -60,6 +64,7 @@ "native-keymap": "2.2.0", "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta17", + "rimraf": "^2.2.8", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", "tas-client-umd": "0.1.2", @@ -161,7 +166,6 @@ "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", - "rimraf": "^2.2.8", "sinon": "^1.17.2", "source-map": "^0.4.4", "style-loader": "^1.0.0", @@ -193,5 +197,8 @@ "windows-foreground-love": "0.2.0", "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" + }, + "resolutions": { + "minimist": "^1.2.5" } } diff --git a/product.json b/product.json index 7cab6d1b9f3b84bfc703856e93773a293fd198cf..31d3d5a943192eee791e1121415b436dc1ed3822 100644 --- a/product.json +++ b/product.json @@ -20,7 +20,7 @@ "darwinBundleIdentifier": "com.visualstudio.code.oss", "linuxIconName": "com.visualstudio.code.oss", "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", + "reportIssueUrl": "https://github.com/cdr/code-server/issues/new", "urlProtocol": "code-oss", "extensionAllowedProposedApi": [ "ms-vscode.vscode-js-profile-flame", diff --git a/remote/.yarnrc b/remote/.yarnrc deleted file mode 100644 index c1a32ce532afa501fb19bdbcf6bcb0ec151ecd99..0000000000000000000000000000000000000000 --- a/remote/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "http://nodejs.org/dist" -target "12.14.1" -runtime "node" diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index f475b10e5e81d5c2511d8d36ca5fa30a54bc415a..e9a30b2cd2a7848241d9a430c28faccb51efdb9b 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -113,16 +113,17 @@ class RemoteAuthoritiesImpl { if (host && host.indexOf(':') !== -1) { host = `[${host}]`; } - const port = this._ports[authority]; + // const port = this._ports[authority]; const connectionToken = this._connectionTokens[authority]; let query = `path=${encodeURIComponent(uri.path)}`; if (typeof connectionToken === 'string') { query += `&tkn=${encodeURIComponent(connectionToken)}`; } + // NOTE@coder: Changed this to work against the current path. return URI.from({ scheme: platform.isWeb ? this._preferredWebSchema : Schemas.vscodeRemoteResource, - authority: `${host}:${port}`, - path: `/vscode-remote-resource`, + authority: window.location.host, + path: `${window.location.pathname.replace(/\/+$/, '')}/vscode-remote-resource`, query }); } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 3361d83be5b7c3d08bdbfbe6947942a4695882c6..69ead8484e042bbad7075659f8e47f074bc217e4 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -71,6 +71,18 @@ if (typeof navigator === 'object' && !isElectronRenderer) { _isWeb = true; _locale = navigator.language; _language = _locale; + + // NOTE@coder: Make languages work. + const el = typeof document !== 'undefined' && document.getElementById('vscode-remote-nls-configuration'); + const rawNlsConfig = el && el.getAttribute('data-settings'); + if (rawNlsConfig) { + try { + const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig); + _locale = nlsConfig.locale; + _translationsConfigFile = nlsConfig._translationsConfigFile; + _language = nlsConfig.availableLanguages['*'] || LANGUAGE_DEFAULT; + } catch (error) { /* Oh well. */ } + } } // Native environment diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index 17895a8510bca40924524dc107c33305c4783c45..ba019b43084e3998ab399108968c3c765a79eb32 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -112,6 +112,7 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve /^VSCODE_.+$/, /^SNAP(|_.*)$/, /^GDK_PIXBUF_.+$/, + /^CODE_SERVER_.+$/, ]; const envKeys = Object.keys(env); envKeys diff --git a/src/vs/base/common/uriIpc.ts b/src/vs/base/common/uriIpc.ts index ef2291d49b13c9c995afc90eab9c92afabc2b3b4..29b2f9dfc2b7fa998ac1188db06dee95419fcd5b 100644 --- a/src/vs/base/common/uriIpc.ts +++ b/src/vs/base/common/uriIpc.ts @@ -5,6 +5,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { MarshalledObject } from 'vs/base/common/marshalling'; +import { Schemas } from './network'; export interface IURITransformer { transformIncoming(uri: UriComponents): UriComponents; @@ -31,29 +32,35 @@ function toJSON(uri: URI): UriComponents { export class URITransformer implements IURITransformer { - private readonly _uriTransformer: IRawURITransformer; - - constructor(uriTransformer: IRawURITransformer) { - this._uriTransformer = uriTransformer; + constructor(private readonly remoteAuthority: string) { } + // NOTE@coder: Coming in from the browser it'll be vscode-remote so it needs + // to be transformed into file. public transformIncoming(uri: UriComponents): UriComponents { - const result = this._uriTransformer.transformIncoming(uri); - return (result === uri ? uri : toJSON(URI.from(result))); + return uri.scheme === Schemas.vscodeRemote + ? toJSON(URI.file(uri.path)) + : uri; } + // NOTE@coder: Going out to the browser it'll be file so it needs to be + // transformed into vscode-remote. public transformOutgoing(uri: UriComponents): UriComponents { - const result = this._uriTransformer.transformOutgoing(uri); - return (result === uri ? uri : toJSON(URI.from(result))); + return uri.scheme === Schemas.file + ? toJSON(URI.from({ authority: this.remoteAuthority, scheme: Schemas.vscodeRemote, path: uri.path })) + : uri; } public transformOutgoingURI(uri: URI): URI { - const result = this._uriTransformer.transformOutgoing(uri); - return (result === uri ? uri : URI.from(result)); + return uri.scheme === Schemas.file + ? URI.from({ authority: this.remoteAuthority, scheme: Schemas.vscodeRemote, path:uri.path }) + : uri; } public transformOutgoingScheme(scheme: string): string { - return this._uriTransformer.transformOutgoingScheme(scheme); + return scheme === Schemas.file + ? Schemas.vscodeRemote + : scheme; } } @@ -152,4 +159,4 @@ export function transformAndReviveIncomingURIs(obj: T, transformer: IURITrans return obj; } return result; -} \ No newline at end of file +} diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js index 2c64061da7b01aef0bfe3cec851da232ca9461c8..c0ef8faedd406c38bf9c55bbbdbbb060046492d9 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -128,7 +128,10 @@ function factory(nodeRequire, path, fs, perf) { function getLanguagePackConfigurations(userDataPath) { const configFile = path.join(userDataPath, 'languagepacks.json'); try { - return nodeRequire(configFile); + // NOTE@coder: Swapped require with readFile since require is cached and + // we don't restart the server-side portion of code-server when the + // language changes. + return JSON.parse(fs.readFileSync(configFile, "utf8")); } catch (err) { // Do nothing. If we can't read the file we have no // language pack config. diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 0ef8b9dc81419b53b27cf111fb206d72ba56bada..62a79602a831bca0dc62ad57dc10a9375f8b9cdb 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -17,6 +17,7 @@ import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; import product from 'vs/platform/product/common/product'; +import { encodePath } from 'vs/server/node/util'; function doCreateUri(path: string, queryValues: Map): URI { let query: string | undefined = undefined; @@ -309,12 +310,18 @@ class WorkspaceProvider implements IWorkspaceProvider { // Folder else if (isFolderToOpen(workspace)) { - targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${encodeURIComponent(workspace.folderUri.toString())}`; + const target = workspace.folderUri.scheme === Schemas.vscodeRemote + ? encodePath(workspace.folderUri.path) + : encodeURIComponent(workspace.folderUri.toString()); + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${target}`; } // Workspace else if (isWorkspaceToOpen(workspace)) { - targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${encodeURIComponent(workspace.workspaceUri.toString())}`; + const target = workspace.workspaceUri.scheme === Schemas.vscodeRemote + ? encodePath(workspace.workspaceUri.path) + : encodeURIComponent(workspace.workspaceUri.toString()); + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${target}`; } // Append payload if any @@ -404,7 +411,22 @@ class WindowIndicator implements IWindowIndicator { throw new Error('Missing web configuration element'); } - const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute); + const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = { + webviewEndpoint: `${window.location.origin}${window.location.pathname.replace(/\/+$/, '')}/webview`, + ...JSON.parse(configElementAttribute), + }; + + // Strip the protocol from the authority if it exists. + const normalizeAuthority = (authority: string): string => authority.replace(/^https?:\/\//, ""); + if (config.remoteAuthority) { + (config as any).remoteAuthority = normalizeAuthority(config.remoteAuthority); + } + if (config.workspaceUri && config.workspaceUri.authority) { + config.workspaceUri.authority = normalizeAuthority(config.workspaceUri.authority); + } + if (config.folderUri && config.folderUri.authority) { + config.folderUri.authority = normalizeAuthority(config.folderUri.authority); + } // Revive static extension locations if (Array.isArray(config.staticExtensions)) { @@ -416,40 +438,7 @@ class WindowIndicator implements IWindowIndicator { // Find workspace to open and payload let foundWorkspace = false; let workspace: IWorkspace; - let payload = Object.create(null); - - const query = new URL(document.location.href).searchParams; - query.forEach((value, key) => { - switch (key) { - - // Folder - case WorkspaceProvider.QUERY_PARAM_FOLDER: - workspace = { folderUri: URI.parse(value) }; - foundWorkspace = true; - break; - - // Workspace - case WorkspaceProvider.QUERY_PARAM_WORKSPACE: - workspace = { workspaceUri: URI.parse(value) }; - foundWorkspace = true; - break; - - // Empty - case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW: - workspace = undefined; - foundWorkspace = true; - break; - - // Payload - case WorkspaceProvider.QUERY_PARAM_PAYLOAD: - try { - payload = JSON.parse(value); - } catch (error) { - console.error(error); // possible invalid JSON - } - break; - } - }); + let payload = config.workspaceProvider?.payload || Object.create(null); // If no workspace is provided through the URL, check for config attribute from server if (!foundWorkspace) { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 409bb7e1960c9c06485a6f6d7f39b2efce451d56..f27b651c49ea3fc92b03e31eb64c1cf27c7e4433 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -7,6 +7,8 @@ * A list of command line arguments we support natively. */ export interface NativeParsedArgs { + 'extra-extensions-dir'?: string[]; + 'extra-builtin-extensions-dir'?: string[]; _: string[]; 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 21b4d719cec1a724bbad407aeec38db9eb8d6f5a..edf46f097bf11bfb8883d38d38ee78b735f35b3f 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -122,6 +122,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { extensionsPath?: string; extensionsDownloadPath: string; builtinExtensionsPath: string; + extraExtensionPaths: string[] + extraBuiltinExtensionPaths: string[] // --- Smoke test support driverHandle?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 149e6ffb41a82f1a69cf37f105a31872ad4af8b4..ed99aab42b31bc2ab804391b6e3f4c7ff67d9259 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -54,6 +54,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, + 'extra-builtin-extensions-dir': { type: 'string[]', cat: 'o', description: 'Path to an extra builtin extension directory.' }, + 'extra-extensions-dir': { type: 'string[]', cat: 'o', description: 'Path to an extra user extension directory.' }, 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, @@ -318,4 +320,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve export function buildVersionMessage(version: string | undefined, commit: string | undefined): string { return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`; } - diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 80f68fb1decfd1c4fa1bcc30840900240df83f76..d4478b0000a511af11647876a536b8147163f9f8 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -138,6 +138,13 @@ export class NativeEnvironmentService implements INativeEnvironmentService { return resources.joinPath(this.userHome, product.dataFolderName, 'extensions').fsPath; } + @memoize get extraExtensionPaths(): string[] { + return (this._args['extra-extensions-dir'] || []).map((p) => parsePathArg(p, process)); + } + @memoize get extraBuiltinExtensionPaths(): string[] { + return (this._args['extra-builtin-extensions-dir'] || []).map((p) => parsePathArg(p, process)); + } + @memoize get extensionDevelopmentLocationURI(): URI[] | undefined { const s = this._args.extensionDevelopmentPath; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index aee65f8eddbfbce3e42362be9590c98d46f2ace5..dc891fba7c7af3ace02b0091ef858bea59e754c6 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -91,7 +91,7 @@ export class ExtensionsScanner extends Disposable { } async scanAllUserExtensions(): Promise { - return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User); + return this.scanExtensionsInDirs(this.extensionsPath, this.environmentService.extraExtensionPaths, ExtensionType.User); } async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise { @@ -236,7 +236,13 @@ export class ExtensionsScanner extends Disposable { private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise { const limiter = new Limiter(10); - const extensionsFolders = await pfs.readdir(dir); + const extensionsFolders = await pfs.readdir(dir) + .catch((error) => { + if (error.code !== 'ENOENT') { + throw error; + } + return []; + }); const extensions = await Promise.all(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type)))); return extensions.filter(e => e && e.identifier); } @@ -266,7 +272,7 @@ export class ExtensionsScanner extends Disposable { } private async scanDefaultSystemExtensions(): Promise { - const result = await this.scanExtensionsInDir(this.systemExtensionsPath, ExtensionType.System); + const result = await this.scanExtensionsInDirs(this.systemExtensionsPath, this.environmentService.extraBuiltinExtensionPaths, ExtensionType.System); this.logService.trace('Scanned system extensions:', result.length); return result; } @@ -370,4 +376,9 @@ export class ExtensionsScanner extends Disposable { } }); } + + private async scanExtensionsInDirs(dir: string, dirs: string[], type: ExtensionType): Promise{ + const results = await Promise.all([dir, ...dirs].map((path) => this.scanExtensionsInDir(path, type))); + return results.reduce((flat, current) => flat.concat(current), []); + } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 2bea85740cb3e00c955ec0f7aa46d5f9bb8d5dc8..c0953d7b73178fc4a7b030246a5281609c3dfce6 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -37,6 +37,12 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire ! ], }); } + // NOTE@coder: Add the ability to inject settings from the server. + const el = document.getElementById('vscode-remote-product-configuration'); + const rawProductConfiguration = el && el.getAttribute('data-settings'); + if (rawProductConfiguration) { + Object.assign(product, JSON.parse(rawProductConfiguration)); + } } // Native (non-sandboxed) diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 333e5b24b05c96e8d44e9025b7a777e6989de9e7..b13572327a6e91592eedea9bcb1e580397f5c224 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -32,6 +32,8 @@ export type ConfigurationSyncStore = { }; export interface IProductConfiguration { + readonly codeServerVersion?: string; + readonly version: string; readonly date?: string; readonly quality?: string; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 3715cbb8e6ee41c3d9b5090918d243b723ae2d00..c65de8ad37e727d66da97a8f8b170cbcef87181b 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -208,7 +208,8 @@ export class BrowserSocketFactory implements ISocketFactory { } connect(host: string, port: number, query: string, callback: IConnectCallback): void { - const socket = this._webSocketFactory.create(`ws://${host}:${port}/?${query}&skipWebSocketFrames=false`); + // NOTE@coder: Modified to work against the current path. + const socket = this._webSocketFactory.create(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}${window.location.pathname}?${query}&skipWebSocketFrames=false`); const errorListener = socket.onError((err) => callback(err, undefined)); socket.onOpen(() => { errorListener.dispose(); @@ -216,6 +217,3 @@ export class BrowserSocketFactory implements ISocketFactory { }); } } - - - diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index fdd5890c69f72025b94913380f0d226226e8c8fb..e084236526b38c1144d47b8b3000b367c3207fe8 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -93,7 +93,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio options.socketFactory.connect( options.host, options.port, - `reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, + `type=${connectionTypeToString(connectionType)}&reconnectionToken=${options.reconnectionToken}&reconnection=${options.reconnectionProtocol ? 'true' : 'false'}`, (err: any, socket: ISocket | undefined) => { if (err || !socket) { options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index ab3fd347b69f8a3d9b96e706cd87c911b8ffed6b..9d351037b577f9f1edfd18ae9b3c48a211f4467f 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -122,8 +122,8 @@ export class BrowserStorageService extends Disposable implements IStorageService return this.getStorage(scope).getNumber(key, fallbackValue); } - store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { - this.getStorage(scope).set(key, value); + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise { + return this.getStorage(scope).set(key, value); } remove(key: string, scope: StorageScope): void { diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 6611f1dae42055f69a55c1c154d9475f11cd4d0a..d598d4909d5ff6d1614e4a038b1865e1f9a4e963 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -85,7 +85,7 @@ export interface IStorageService { * The scope argument allows to define the scope of the storage * operation to either the current workspace only or all workspaces. */ - store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void; + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise | void; /** * Delete an element stored under the provided key from storage. diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 096b9e23493539c9937940a56e555d95bbae38d9..ef37e614004f550f7b64eacd362f6894fc523a42 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -201,8 +201,8 @@ export class NativeStorageService extends Disposable implements IStorageService return this.getStorage(scope).getNumber(key, fallbackValue); } - store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { - this.getStorage(scope).set(key, value); + store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise { + return this.getStorage(scope).set(key, value); } remove(key: string, scope: StorageScope): void { diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee93765d71601 --- /dev/null +++ b/src/vs/server/browser/client.ts @@ -0,0 +1,189 @@ +import { Emitter } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { INodeProxyService, NodeProxyChannelClient } from 'vs/server/common/nodeProxy'; +import { TelemetryChannelClient } from 'vs/server/common/telemetry'; +import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; +import { LocalizationsService } from 'vs/workbench/services/localizations/electron-browser/localizationsService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Options } from 'vs/server/ipc.d'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; + +class TelemetryService extends TelemetryChannelClient { + public constructor( + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + ) { + super(remoteAgentService.getConnection()!.getChannel('telemetry')); + } +} + +/** + * Remove extra slashes in a URL. + */ +export const normalize = (url: string, keepTrailing = false): string => { + return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : ""); +}; + +/** + * Get options embedded in the HTML. + */ +export const getOptions = (): T => { + try { + return JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!); + } catch (error) { + return {} as T; + } +}; + +const options = getOptions(); + +const TELEMETRY_SECTION_ID = 'telemetry'; +Registry.as(Extensions.Configuration).registerConfiguration({ + 'id': TELEMETRY_SECTION_ID, + 'order': 110, + 'type': 'object', + 'title': localize('telemetryConfigurationTitle', 'Telemetry'), + 'properties': { + 'telemetry.enableTelemetry': { + 'type': 'boolean', + 'description': localize('telemetry.enableTelemetry', 'Enable usage data and errors to be sent to a Microsoft online service.'), + 'default': !options.disableTelemetry, + 'tags': ['usesOnlineServices'] + } + } +}); + +class NodeProxyService extends NodeProxyChannelClient implements INodeProxyService { + private readonly _onClose = new Emitter(); + public readonly onClose = this._onClose.event; + private readonly _onDown = new Emitter(); + public readonly onDown = this._onDown.event; + private readonly _onUp = new Emitter(); + public readonly onUp = this._onUp.event; + + public constructor( + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + ) { + super(remoteAgentService.getConnection()!.getChannel('nodeProxy')); + remoteAgentService.getConnection()!.onDidStateChange((state) => { + switch (state.type) { + case PersistentConnectionEventType.ConnectionGain: + return this._onUp.fire(); + case PersistentConnectionEventType.ConnectionLost: + return this._onDown.fire(); + case PersistentConnectionEventType.ReconnectionPermanentFailure: + return this._onClose.fire(); + } + }); + } +} + +registerSingleton(ILocalizationsService, LocalizationsService); +registerSingleton(INodeProxyService, NodeProxyService); +registerSingleton(ITelemetryService, TelemetryService); + +/** + * This is called by vs/workbench/browser/web.main.ts after the workbench has + * been initialized so we can initialize our own client-side code. + */ +export const initialize = async (services: ServiceCollection): Promise => { + const event = new CustomEvent('ide-ready'); + window.dispatchEvent(event); + + if (parent) { + // Tell the parent loading has completed. + parent.postMessage({ event: 'loaded' }, window.location.origin); + + // Proxy or stop proxing events as requested by the parent. + const listeners = new Map void>(); + window.addEventListener('message', (parentEvent) => { + const eventName = parentEvent.data.bind || parentEvent.data.unbind; + if (eventName) { + const oldListener = listeners.get(eventName); + if (oldListener) { + document.removeEventListener(eventName, oldListener); + } + } + + if (parentEvent.data.bind && parentEvent.data.prop) { + const listener = (event: Event) => { + parent.postMessage({ + event: parentEvent.data.event, + [parentEvent.data.prop]: event[parentEvent.data.prop as keyof Event] + }, window.location.origin); + }; + listeners.set(parentEvent.data.bind, listener); + document.addEventListener(parentEvent.data.bind, listener); + } + }); + } + + if (!window.isSecureContext) { + (services.get(INotificationService) as INotificationService).notify({ + severity: Severity.Warning, + message: 'code-server is being accessed over an insecure domain. Web views, the clipboard, and other functionality will not work as expected.', + actions: { + primary: [{ + id: 'understand', + label: 'I understand', + tooltip: '', + class: undefined, + enabled: true, + checked: true, + dispose: () => undefined, + run: () => { + return Promise.resolve(); + } + }], + } + }); + } + + // This will be used to set the background color while VS Code loads. + const theme = (services.get(IStorageService) as IStorageService).get("colorThemeData", StorageScope.GLOBAL); + if (theme) { + localStorage.setItem("colorThemeData", theme); + } +}; + +export interface Query { + [key: string]: string | undefined; +} + +/** + * Split a string up to the delimiter. If the delimiter doesn't exist the first + * item will have all the text and the second item will be an empty string. + */ +export const split = (str: string, delimiter: string): [string, string] => { + const index = str.indexOf(delimiter); + return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, '']; +}; + +/** + * Return the URL modified with the specified query variables. It's pretty + * stupid so it probably doesn't cover any edge cases. Undefined values will + * unset existing values. Doesn't allow duplicates. + */ +export const withQuery = (url: string, replace: Query): string => { + const uri = URI.parse(url); + const query = { ...replace }; + uri.query.split('&').forEach((kv) => { + const [key, value] = split(kv, '='); + if (!(key in query)) { + query[key] = value; + } + }); + return uri.with({ + query: Object.keys(query) + .filter((k) => typeof query[k] !== 'undefined') + .map((k) => `${k}=${query[k]}`).join('&'), + }).toString(true); +}; diff --git a/src/vs/server/browser/extHostNodeProxy.ts b/src/vs/server/browser/extHostNodeProxy.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dd5406befcb593ad6366d9e98f46485ed14fbc0 --- /dev/null +++ b/src/vs/server/browser/extHostNodeProxy.ts @@ -0,0 +1,51 @@ +import { Emitter } from 'vs/base/common/event'; +import { UriComponents } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ExtHostNodeProxyShape, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; + +export class ExtHostNodeProxy implements ExtHostNodeProxyShape { + _serviceBrand: any; + + private readonly _onMessage = new Emitter(); + public readonly onMessage = this._onMessage.event; + private readonly _onClose = new Emitter(); + public readonly onClose = this._onClose.event; + private readonly _onDown = new Emitter(); + public readonly onDown = this._onDown.event; + private readonly _onUp = new Emitter(); + public readonly onUp = this._onUp.event; + + private readonly proxy: MainThreadNodeProxyShape; + + constructor(@IExtHostRpcService rpc: IExtHostRpcService) { + this.proxy = rpc.getProxy(MainContext.MainThreadNodeProxy); + } + + public $onMessage(message: string): void { + this._onMessage.fire(message); + } + + public $onClose(): void { + this._onClose.fire(); + } + + public $onUp(): void { + this._onUp.fire(); + } + + public $onDown(): void { + this._onDown.fire(); + } + + public send(message: string): void { + this.proxy.$send(message); + } + + public async fetchExtension(extensionUri: UriComponents): Promise { + return this.proxy.$fetchExtension(extensionUri).then(b => b.buffer); + } +} + +export interface IExtHostNodeProxy extends ExtHostNodeProxy { } +export const IExtHostNodeProxy = createDecorator('IExtHostNodeProxy'); diff --git a/src/vs/server/browser/mainThreadNodeProxy.ts b/src/vs/server/browser/mainThreadNodeProxy.ts new file mode 100644 index 0000000000000000000000000000000000000000..21a139288e5b8f56016491879d69d01da929decb --- /dev/null +++ b/src/vs/server/browser/mainThreadNodeProxy.ts @@ -0,0 +1,55 @@ +import { VSBuffer } from 'vs/base/common/buffer'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess } from 'vs/base/common/network'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { INodeProxyService } from 'vs/server/common/nodeProxy'; +import { ExtHostContext, IExtHostContext, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; + +@extHostNamedCustomer(MainContext.MainThreadNodeProxy) +export class MainThreadNodeProxy implements MainThreadNodeProxyShape { + private disposed = false; + private disposables = []; + + constructor( + extHostContext: IExtHostContext, + @INodeProxyService private readonly proxyService: INodeProxyService, + ) { + if (!extHostContext.remoteAuthority) { // HACK: A terrible way to detect if running in the worker. + const proxy = extHostContext.getProxy(ExtHostContext.ExtHostNodeProxy); + this.disposables = [ + this.proxyService.onMessage((message: string) => proxy.$onMessage(message)), + this.proxyService.onClose(() => proxy.$onClose()), + this.proxyService.onDown(() => proxy.$onDown()), + this.proxyService.onUp(() => proxy.$onUp()), + ]; + } + } + + $send(message: string): void { + if (!this.disposed) { + this.proxyService.send(message); + } + } + + async $fetchExtension(extensionUri: UriComponents): Promise { + const fetchUri = URI.from({ + scheme: window.location.protocol.replace(':', ''), + authority: window.location.host, + // Use FileAccess to get the static base path. + path: FileAccess.asBrowserUri("", require).path, + query: `tar=${encodeURIComponent(extensionUri.path)}`, + }); + const response = await fetch(fetchUri.toString(true)); + if (response.status !== 200) { + throw new Error(`Failed to download extension "${module}"`); + } + return VSBuffer.wrap(new Uint8Array(await response.arrayBuffer())); + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + this.disposables = []; + this.disposed = true; + } +} diff --git a/src/vs/server/browser/worker.ts b/src/vs/server/browser/worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d47ede49b76b1774329269ab5c86fedb5712c19 --- /dev/null +++ b/src/vs/server/browser/worker.ts @@ -0,0 +1,48 @@ +import { Client } from '@coder/node-browser'; +import { fromTar } from '@coder/requirefs'; +import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { IExtHostNodeProxy } from './extHostNodeProxy'; + +export const loadCommonJSModule = async ( + module: URI, + activationTimesBuilder: ExtensionActivationTimesBuilder, + nodeProxy: IExtHostNodeProxy, + logService: ILogService, + vscode: any, +): Promise => { + const client = new Client(nodeProxy, { logger: logService }); + const [buffer, init] = await Promise.all([ + nodeProxy.fetchExtension(module), + client.handshake(), + ]); + const rfs = fromTar(buffer); + (self).global = self; + rfs.provide('vscode', vscode); + Object.keys(client.modules).forEach((key) => { + const mod = (client.modules as any)[key]; + if (key === 'process') { + (self).process = mod; + (self).process.env = init.env; + return; + } + + rfs.provide(key, mod); + switch (key) { + case 'buffer': + (self).Buffer = mod.Buffer; + break; + case 'timers': + (self).setImmediate = mod.setImmediate; + break; + } + }); + + try { + activationTimesBuilder.codeLoadingStart(); + return rfs.require('.'); + } finally { + activationTimesBuilder.codeLoadingStop(); + } +}; diff --git a/src/vs/server/common/nodeProxy.ts b/src/vs/server/common/nodeProxy.ts new file mode 100644 index 0000000000000000000000000000000000000000..14b9de879ceab4c1976770fa7810d276c5aa3e36 --- /dev/null +++ b/src/vs/server/common/nodeProxy.ts @@ -0,0 +1,47 @@ +import { ReadWriteConnection } from '@coder/node-browser'; +import { Event } from 'vs/base/common/event'; +import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const INodeProxyService = createDecorator('nodeProxyService'); + +export interface INodeProxyService extends ReadWriteConnection { + _serviceBrand: any; + send(message: string): void; + onMessage: Event; + onUp: Event; + onClose: Event; + onDown: Event; +} + +export class NodeProxyChannel implements IServerChannel { + constructor(private service: INodeProxyService) {} + + listen(_: unknown, event: string): Event { + switch (event) { + case 'onMessage': return this.service.onMessage; + } + throw new Error(`Invalid listen ${event}`); + } + + async call(_: unknown, command: string, args?: any): Promise { + switch (command) { + case 'send': return this.service.send(args[0]); + } + throw new Error(`Invalid call ${command}`); + } +} + +export class NodeProxyChannelClient { + _serviceBrand: any; + + public readonly onMessage: Event; + + constructor(private readonly channel: IChannel) { + this.onMessage = this.channel.listen('onMessage'); + } + + public send(data: string): void { + this.channel.call('send', [data]); + } +} diff --git a/src/vs/server/common/telemetry.ts b/src/vs/server/common/telemetry.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ea6d95d36aaac07dbd4d0e16ab3c1bba255f683 --- /dev/null +++ b/src/vs/server/common/telemetry.ts @@ -0,0 +1,65 @@ +import { ITelemetryData } from 'vs/base/common/actions'; +import { Event } from 'vs/base/common/event'; +import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +export class TelemetryChannel implements IServerChannel { + constructor(private service: ITelemetryService) {} + + listen(_: unknown, event: string): Event { + throw new Error(`Invalid listen ${event}`); + } + + call(_: unknown, command: string, args?: any): Promise { + switch (command) { + case 'publicLog': return this.service.publicLog(args[0], args[1], args[2]); + case 'publicLog2': return this.service.publicLog2(args[0], args[1], args[2]); + case 'publicLogError': return this.service.publicLogError(args[0], args[1]); + case 'publicLogError2': return this.service.publicLogError2(args[0], args[1]); + case 'setEnabled': return Promise.resolve(this.service.setEnabled(args[0])); + case 'getTelemetryInfo': return this.service.getTelemetryInfo(); + case 'setExperimentProperty': return Promise.resolve(this.service.setExperimentProperty(args[0], args[1])); + } + throw new Error(`Invalid call ${command}`); + } +} + +export class TelemetryChannelClient implements ITelemetryService { + _serviceBrand: any; + + // These don't matter; telemetry is sent to the Node side which decides + // whether to send the telemetry event. + public isOptedIn = true; + public sendErrorTelemetry = true; + + constructor(private readonly channel: IChannel) {} + + public publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { + return this.channel.call('publicLog', [eventName, data, anonymizeFilePaths]); + } + + public publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise { + return this.channel.call('publicLog2', [eventName, data, anonymizeFilePaths]); + } + + public publicLogError(errorEventName: string, data?: ITelemetryData): Promise { + return this.channel.call('publicLogError', [errorEventName, data]); + } + + public publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck): Promise { + return this.channel.call('publicLogError2', [eventName, data]); + } + + public setEnabled(value: boolean): void { + this.channel.call('setEnable', [value]); + } + + public getTelemetryInfo(): Promise { + return this.channel.call('getTelemetryInfo'); + } + + public setExperimentProperty(name: string, value: string): void { + this.channel.call('setExperimentProperty', [name, value]); + } +} diff --git a/src/vs/server/entry.ts b/src/vs/server/entry.ts new file mode 100644 index 0000000000000000000000000000000000000000..8482c48bae007ed6b39183001ae2cc6d140fcd50 --- /dev/null +++ b/src/vs/server/entry.ts @@ -0,0 +1,79 @@ +import { field } from '@coder/logger'; +import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { CodeServerMessage, VscodeMessage } from 'vs/server/ipc'; +import { logger } from 'vs/server/node/logger'; +import { enableCustomMarketplace } from 'vs/server/node/marketplace'; +import { Vscode } from 'vs/server/node/server'; + +setUnexpectedErrorHandler((error) => logger.warn(error instanceof Error ? error.message : error)); +enableCustomMarketplace(); + +/** + * Ensure we control when the process exits. + */ +const exit = process.exit; +process.exit = function(code?: number) { + logger.warn(`process.exit() was prevented: ${code || 'unknown code'}.`); +} as (code?: number) => never; + +// Kill VS Code if the parent process dies. +if (typeof process.env.CODE_SERVER_PARENT_PID !== 'undefined') { + const parentPid = parseInt(process.env.CODE_SERVER_PARENT_PID, 10); + setInterval(() => { + try { + process.kill(parentPid, 0); // Throws an exception if the process doesn't exist anymore. + } catch (e) { + exit(); + } + }, 5000); +} else { + logger.error('no parent process'); + exit(1); +} + +const vscode = new Vscode(); +const send = (message: VscodeMessage): void => { + if (!process.send) { + throw new Error('not spawned with IPC'); + } + process.send(message); +}; + +// 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('type', message.type)); + logger.trace('code-server message content', field('message', message)); + switch (message.type) { + case 'init': + try { + const options = await vscode.initialize(message.options); + send({ type: 'options', id: message.id, options }); + } catch (error) { + logger.error(error.message); + logger.error(error.stack); + exit(1); + } + break; + case 'cli': + try { + await vscode.cli(message.args); + exit(0); + } catch (error) { + logger.error(error.message); + logger.error(error.stack); + exit(1); + } + break; + case 'socket': + vscode.handleWebSocket(socket, message.query); + break; + } +}); +if (!process.send) { + logger.error('not spawned with IPC'); + exit(1); +} else { + // This lets the parent know the child is ready to receive messages. + send({ type: 'ready' }); +} diff --git a/src/vs/server/fork.js b/src/vs/server/fork.js new file mode 100644 index 0000000000000000000000000000000000000000..56331ff1fc32bbd82e769aaecb551e427f798ec3 --- /dev/null +++ b/src/vs/server/fork.js @@ -0,0 +1,3 @@ +// This must be a JS file otherwise when it gets compiled it turns into AMD +// syntax which will not work without the right loader. +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 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78fc72a5c6 --- /dev/null +++ b/src/vs/server/ipc.d.ts @@ -0,0 +1,131 @@ +/** + * External interfaces for integration into code-server over IPC. No vs imports + * should be made in this file. + */ +export interface Options { + disableTelemetry: boolean +} + +export interface InitMessage { + type: 'init'; + id: string; + options: VscodeOptions; +} + +export type Query = { [key: string]: string | string[] | undefined | Query | Query[] }; + +export interface SocketMessage { + type: 'socket'; + query: Query; +} + +export interface CliMessage { + type: 'cli'; + args: Args; +} + +export interface OpenCommandPipeArgs { + type: 'open'; + fileURIs?: string[]; + folderURIs: string[]; + forceNewWindow?: boolean; + diffMode?: boolean; + addMode?: boolean; + gotoLineMode?: boolean; + forceReuseWindow?: boolean; + waitMarkerFilePath?: string; +} + +export type CodeServerMessage = InitMessage | SocketMessage | CliMessage; + +export interface ReadyMessage { + type: 'ready'; +} + +export interface OptionsMessage { + id: string; + type: 'options'; + options: WorkbenchOptions; +} + +export type VscodeMessage = ReadyMessage | OptionsMessage; + +export interface StartPath { + url: string; + workspace: boolean; +} + +export interface Args { + 'user-data-dir'?: string; + + 'enable-proposed-api'?: string[]; + 'extensions-dir'?: string; + 'builtin-extensions-dir'?: string; + 'extra-extensions-dir'?: string[]; + 'extra-builtin-extensions-dir'?: string[]; + + locale?: string + + log?: string; + verbose?: boolean; + + _: string[]; +} + +export interface VscodeOptions { + readonly args: Args; + readonly remoteAuthority: string; + readonly startPath?: StartPath; +} + +export interface VscodeOptionsMessage extends VscodeOptions { + readonly id: string; +} + +export interface UriComponents { + readonly scheme: string; + readonly authority: string; + readonly path: string; + readonly query: string; + readonly fragment: string; +} + +export interface NLSConfiguration { + locale: string; + availableLanguages: { + [key: string]: string; + }; + pseudo?: boolean; + _languagePackSupport?: boolean; +} + +export interface WorkbenchOptions { + readonly workbenchWebConfiguration: { + readonly remoteAuthority?: string; + readonly folderUri?: UriComponents; + readonly workspaceUri?: UriComponents; + readonly logLevel?: number; + readonly workspaceProvider?: { + payload: [ + ["userDataPath", string], + ["enableProposedApi", string], + ]; + }; + }; + readonly remoteUserDataUri: UriComponents; + readonly productConfiguration: { + codeServerVersion?: string; + readonly extensionsGallery?: { + readonly serviceUrl: string; + readonly itemUrl: string; + readonly controlUrl: string; + readonly recommendationsUrl: string; + }; + }; + readonly nlsConfiguration: NLSConfiguration; + readonly commit: string; +} + +export interface WorkbenchOptionsMessage { + id: string; +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 index 0000000000000000000000000000000000000000..693174ee0d21353c3a08a42fd30eaad1e95c3b9d --- /dev/null +++ b/src/vs/server/node/channel.ts @@ -0,0 +1,897 @@ +import { field, logger } from '@coder/logger'; +import { Server } from '@coder/node-browser'; +import * as os from 'os'; +import * as path from 'path'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; +import * as resources from 'vs/base/common/resources'; +import { ReadableStreamEventPayload } from 'vs/base/common/stream'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileType, FileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { ILogService } from 'vs/platform/log/common/log'; +import product from 'vs/platform/product/common/product'; +import { IRemoteAgentEnvironment, RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { INodeProxyService } from 'vs/server/common/nodeProxy'; +import { getTranslations } from 'vs/server/node/nls'; +import { getUriTransformer } from 'vs/server/node/util'; +import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import * as terminal from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { IShellLaunchConfig, ITerminalEnvironment, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; +import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; +import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; +import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; +import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { ExtensionScanner, ExtensionScannerInput } from 'vs/workbench/services/extensions/node/extensionPoints'; + +/** + * Extend the file provider to allow unwatching. + */ +class Watcher extends DiskFileSystemProvider { + public readonly watches = new Map(); + + public dispose(): void { + this.watches.forEach((w) => w.dispose()); + this.watches.clear(); + super.dispose(); + } + + public _watch(req: number, resource: URI, opts: IWatchOptions): void { + this.watches.set(req, this.watch(resource, opts)); + } + + public unwatch(req: number): void { + this.watches.get(req)!.dispose(); + this.watches.delete(req); + } +} + +export class FileProviderChannel implements IServerChannel, IDisposable { + private readonly provider: DiskFileSystemProvider; + private readonly watchers = new Map(); + + public constructor( + private readonly environmentService: INativeEnvironmentService, + private readonly logService: ILogService, + ) { + this.provider = new DiskFileSystemProvider(this.logService); + } + + public listen(context: RemoteAgentConnectionContext, event: string, args?: any): Event { + switch (event) { + case 'filechange': return this.filechange(context, args[0]); + case 'readFileStream': return this.readFileStream(args[0], args[1]); + } + + throw new Error(`Invalid listen '${event}'`); + } + + private filechange(context: RemoteAgentConnectionContext, session: string): Event { + const emitter = new Emitter({ + onFirstListenerAdd: () => { + const provider = new Watcher(this.logService); + this.watchers.set(session, provider); + const transformer = getUriTransformer(context.remoteAuthority); + provider.onDidChangeFile((events) => { + emitter.fire(events.map((event) => ({ + ...event, + resource: transformer.transformOutgoing(event.resource), + }))); + }); + provider.onDidErrorOccur((event) => this.logService.error(event)); + }, + onLastListenerRemove: () => { + this.watchers.get(session)!.dispose(); + this.watchers.delete(session); + }, + }); + + return emitter.event; + } + + private readFileStream(resource: UriComponents, opts: FileReadStreamOptions): Event> { + const cts = new CancellationTokenSource(); + const fileStream = this.provider.readFileStream(this.transform(resource), opts, cts.token); + const emitter = new Emitter>({ + onFirstListenerAdd: () => { + fileStream.on('data', (data) => emitter.fire(VSBuffer.wrap(data))); + fileStream.on('error', (error) => emitter.fire(error)); + fileStream.on('end', () => emitter.fire('end')); + }, + onLastListenerRemove: () => cts.cancel(), + }); + + return emitter.event; + } + + public call(_: unknown, command: string, args?: any): Promise { + switch (command) { + case 'stat': return this.stat(args[0]); + case 'open': return this.open(args[0], args[1]); + case 'close': return this.close(args[0]); + case 'read': return this.read(args[0], args[1], args[2]); + case 'readFile': return this.readFile(args[0]); + case 'write': return this.write(args[0], args[1], args[2], args[3], args[4]); + case 'writeFile': return this.writeFile(args[0], args[1], args[2]); + case 'delete': return this.delete(args[0], args[1]); + case 'mkdir': return this.mkdir(args[0]); + case 'readdir': return this.readdir(args[0]); + case 'rename': return this.rename(args[0], args[1], args[2]); + case 'copy': return this.copy(args[0], args[1], args[2]); + case 'watch': return this.watch(args[0], args[1], args[2], args[3]); + case 'unwatch': return this.unwatch(args[0], args[1]); + } + + throw new Error(`Invalid call '${command}'`); + } + + public dispose(): void { + this.watchers.forEach((w) => w.dispose()); + this.watchers.clear(); + } + + private async stat(resource: UriComponents): Promise { + return this.provider.stat(this.transform(resource)); + } + + private async open(resource: UriComponents, opts: FileOpenOptions): Promise { + return this.provider.open(this.transform(resource), opts); + } + + private async close(fd: number): Promise { + return this.provider.close(fd); + } + + private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> { + const buffer = VSBuffer.alloc(length); + const bytesRead = await this.provider.read(fd, pos, buffer.buffer, 0, length); + return [buffer, bytesRead]; + } + + private async readFile(resource: UriComponents): Promise { + return VSBuffer.wrap(await this.provider.readFile(this.transform(resource))); + } + + private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise { + return this.provider.write(fd, pos, buffer.buffer, offset, length); + } + + private writeFile(resource: UriComponents, buffer: VSBuffer, opts: FileWriteOptions): Promise { + return this.provider.writeFile(this.transform(resource), buffer.buffer, opts); + } + + private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise { + return this.provider.delete(this.transform(resource), opts); + } + + private async mkdir(resource: UriComponents): Promise { + return this.provider.mkdir(this.transform(resource)); + } + + private async readdir(resource: UriComponents): Promise<[string, FileType][]> { + return this.provider.readdir(this.transform(resource)); + } + + private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + return this.provider.rename(this.transform(resource), URI.from(target), opts); + } + + private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + return this.provider.copy(this.transform(resource), URI.from(target), opts); + } + + private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise { + this.watchers.get(session)!._watch(req, this.transform(resource), opts); + } + + private async unwatch(session: string, req: number): Promise { + this.watchers.get(session)!.unwatch(req); + } + + private transform(resource: UriComponents): URI { + // Used for walkthrough content. + if (/^\/static[^/]*\//.test(resource.path)) { + return URI.file(this.environmentService.appRoot + resource.path.replace(/^\/static[^/]*\//, '/')); + // Used by the webview service worker to load resources. + } else if (resource.path === '/vscode-resource' && resource.query) { + try { + const query = JSON.parse(resource.query); + if (query.requestResourcePath) { + return URI.file(query.requestResourcePath); + } + } catch (error) { /* Carry on. */ } + } + return URI.from(resource); + } +} + +// See ../../workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +export class ExtensionEnvironmentChannel implements IServerChannel { + public constructor( + private readonly environment: INativeEnvironmentService, + private readonly log: ILogService, + private readonly telemetry: ITelemetryService, + private readonly connectionToken: string, + ) {} + + public listen(_: unknown, event: string): Event { + throw new Error(`Invalid listen '${event}'`); + } + + public async call(context: any, command: string, args: any): Promise { + switch (command) { + case 'getEnvironmentData': + return transformOutgoingURIs( + await this.getEnvironmentData(), + getUriTransformer(context.remoteAuthority), + ); + case 'scanExtensions': + return transformOutgoingURIs( + await this.scanExtensions(args.language), + getUriTransformer(context.remoteAuthority), + ); + case 'getDiagnosticInfo': return this.getDiagnosticInfo(); + case 'disableTelemetry': return this.disableTelemetry(); + case 'logTelemetry': return this.logTelemetry(args[0], args[1]); + case 'flushTelemetry': return this.flushTelemetry(); + } + throw new Error(`Invalid call '${command}'`); + } + + private async getEnvironmentData(): Promise { + return { + pid: process.pid, + connectionToken: this.connectionToken, + appRoot: URI.file(this.environment.appRoot), + settingsPath: this.environment.settingsResource, + logsPath: URI.file(this.environment.logsPath), + extensionsPath: URI.file(this.environment.extensionsPath!), + extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, 'extension-host')), + globalStorageHome: this.environment.globalStorageHome, + workspaceStorageHome: this.environment.workspaceStorageHome, + userHome: this.environment.userHome, + os: platform.OS, + }; + } + + private async scanExtensions(language: string): Promise { + const translations = await getTranslations(language, this.environment.userDataPath); + + const scanMultiple = (isBuiltin: boolean, isUnderDevelopment: boolean, paths: string[]): Promise => { + return Promise.all(paths.map((path) => { + return ExtensionScanner.scanExtensions(new ExtensionScannerInput( + product.version, + product.commit, + language, + !!process.env.VSCODE_DEV, + path, + isBuiltin, + isUnderDevelopment, + translations, + ), this.log); + })); + }; + + const scanBuiltin = async (): Promise => { + return scanMultiple(true, false, [this.environment.builtinExtensionsPath, ...this.environment.extraBuiltinExtensionPaths]); + }; + + const scanInstalled = async (): Promise => { + return scanMultiple(false, true, [this.environment.extensionsPath!, ...this.environment.extraExtensionPaths]); + }; + + return Promise.all([scanBuiltin(), scanInstalled()]).then((allExtensions) => { + const uniqueExtensions = new Map(); + allExtensions.forEach((multipleExtensions) => { + multipleExtensions.forEach((extensions) => { + extensions.forEach((extension) => { + const id = ExtensionIdentifier.toKey(extension.identifier); + if (uniqueExtensions.has(id)) { + const oldPath = uniqueExtensions.get(id)!.extensionLocation.fsPath; + const newPath = extension.extensionLocation.fsPath; + this.log.warn(`${oldPath} has been overridden ${newPath}`); + } + uniqueExtensions.set(id, { + ...extension, + // Force extensions that should run on the client due to latency + // issues. + extensionKind: extension.identifier.value === 'vscodevim.vim' + ? [ 'web' ] + : extension.extensionKind, + }); + }); + }); + }); + return Array.from(uniqueExtensions.values()); + }); + } + + private getDiagnosticInfo(): Promise { + throw new Error('not implemented'); + } + + private async disableTelemetry(): Promise { + this.telemetry.setEnabled(false); + } + + private async logTelemetry(eventName: string, data: ITelemetryData): Promise { + this.telemetry.publicLog(eventName, data); + } + + private async flushTelemetry(): Promise { + // We always send immediately at the moment. + } +} + +export class NodeProxyService implements INodeProxyService { + public _serviceBrand = undefined; + + public readonly server: Server; + + private readonly _onMessage = new Emitter(); + public readonly onMessage = this._onMessage.event; + private readonly _$onMessage = new Emitter(); + public readonly $onMessage = this._$onMessage.event; + public readonly _onDown = new Emitter(); + public readonly onDown = this._onDown.event; + public readonly _onUp = new Emitter(); + public readonly onUp = this._onUp.event; + + // Unused because the server connection will never permanently close. + private readonly _onClose = new Emitter(); + public readonly onClose = this._onClose.event; + + public constructor() { + // TODO: down/up + this.server = new Server({ + onMessage: this.$onMessage, + onClose: this.onClose, + onDown: this.onDown, + onUp: this.onUp, + send: (message: string): void => { + this._onMessage.fire(message); + } + }); + } + + public send(message: string): void { + this._$onMessage.fire(message); + } +} + +class VariableResolverService extends AbstractVariableResolverService { + constructor( + remoteAuthority: string, + args: terminal.ICreateTerminalProcessArguments, + env: platform.IProcessEnvironment, + ) { + super({ + getFolderUri: (name: string): URI | undefined => { + const folder = args.workspaceFolders.find((f) => f.name === name); + return folder && URI.revive(folder.uri); + }, + getWorkspaceFolderCount: (): number => { + return args.workspaceFolders.length; + }, + // In ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts it + // looks like there are `config:` entries which must be for this? Not sure + // how/if the URI comes into play though. + getConfigurationValue: (_: URI, section: string): string | undefined => { + return args.resolvedVariables[`config:${section}`]; + }, + getExecPath: (): string | undefined => { + // Assuming that resolverEnv is just for use in the resolver and not for + // the terminal itself. + return (args.resolverEnv && args.resolverEnv['VSCODE_EXEC_PATH']) || env['VSCODE_EXEC_PATH']; + }, + // This is just a guess; this is the only file-related thing we're sent + // and none of these resolver methods seem to get called so I don't know + // how to test. + getFilePath: (): string | undefined => { + const resource = transformIncoming(remoteAuthority, args.activeFileResource); + if (!resource) { + return undefined; + } + // See ../../editor/standalone/browser/simpleServices.ts; + // `BaseConfigurationResolverService` calls `getUriLabel` from there. + if (resource.scheme === 'file') { + return resource.fsPath; + } + return resource.path; + }, + // It looks like these are set here although they aren't on the types: + // ../../workbench/contrib/terminal/common/remoteTerminalChannel.ts + getSelectedText: (): string | undefined => { + return args.resolvedVariables.selectedText; + }, + getLineNumber: (): string | undefined => { + return args.resolvedVariables.selectedText; + }, + }, undefined, env); + } +} + +class Terminal { + private readonly process: TerminalProcess; + private _pid: number = -1; + private _title: string = ""; + public readonly workspaceId: string; + public readonly workspaceName: string; + private readonly persist: boolean; + + private readonly _onDispose = new Emitter(); + public get onDispose(): Event { return this._onDispose.event; } + + // These are replayed when a client reconnects. + private cols: number; + private rows: number; + private replayData: string[] = []; + // This is based on string length and is pretty arbitrary. + private readonly maxReplayData = 10000; + private totalReplayData = 0; + + // According to the release notes the terminals are supposed to dispose after + // a short timeout; in our case we'll use 48 hours so you can get them back + // the next day or over the weekend. + private disposeTimeout: NodeJS.Timeout | undefined; + private disposeDelay = 48 * 60 * 60 * 1000; + + private buffering = false; + private readonly _onEvent = new Emitter({ + // Don't bind to data until something is listening. + onFirstListenerAdd: () => { + logger.debug('Terminal bound', field('id', this.id)); + if (!this.buffering) { + this.buffering = true; + this.bufferer.startBuffering(this.id, this.process.onProcessData); + } + }, + + // Replay stored events. + onFirstListenerDidAdd: () => { + // We only need to replay if the terminal is being reconnected which is + // true if there is a dispose timeout. + if (typeof this.disposeTimeout !== "undefined") { + return; + } + + clearTimeout(this.disposeTimeout); + this.disposeTimeout = undefined; + + logger.debug('Terminal replaying', field('id', this.id)); + this._onEvent.fire({ + type: 'replay', + events: [{ + cols: this.cols, + rows: this.rows, + data: this.replayData.join(""), + }] + }); + }, + + onLastListenerRemove: () => { + logger.debug('Terminal unbound', field('id', this.id)); + if (!this.persist) { // Used by debug consoles. + this.dispose(); + } else { + this.disposeTimeout = setTimeout(() => { + this.dispose(); + }, this.disposeDelay); + } + } + }); + + public get onEvent(): Event { return this._onEvent.event; } + + // Buffer to reduce the number of messages going to the renderer. + private readonly bufferer = new TerminalDataBufferer((_, data) => { + this._onEvent.fire({ + type: 'data', + data, + }); + + // No need to store data if we aren't persisting. + if (!this.persist) { + return; + } + + this.replayData.push(data); + this.totalReplayData += data.length; + + let overflow = this.totalReplayData - this.maxReplayData; + if (overflow <= 0) { + return; + } + + // Drop events until doing so would put us under budget. + let deleteCount = 0; + for (; deleteCount < this.replayData.length + && this.replayData[deleteCount].length <= overflow; ++deleteCount) { + overflow -= this.replayData[deleteCount].length; + } + + if (deleteCount > 0) { + this.replayData.splice(0, deleteCount); + } + + // Dropping any more events would put us under budget; trim the first event + // instead if still over budget. + if (overflow > 0 && this.replayData.length > 0) { + this.replayData[0] = this.replayData[0].substring(overflow); + } + + this.totalReplayData = this.replayData.reduce((p, c) => p + c.length, 0); + }); + + public get pid(): number { + return this._pid; + } + + public get title(): string { + return this._title; + } + + public constructor( + public readonly id: number, + config: IShellLaunchConfig & { cwd: string }, + args: terminal.ICreateTerminalProcessArguments, + env: platform.IProcessEnvironment, + logService: ILogService, + ) { + this.workspaceId = args.workspaceId; + this.workspaceName = args.workspaceName; + + this.cols = args.cols; + this.rows = args.rows; + + // TODO: Don't persist terminals until we make it work with things like + // htop, vim, etc. + // this.persist = args.shouldPersistTerminal; + this.persist = false; + + this.process = new TerminalProcess( + config, + config.cwd, + this.cols, + this.rows, + env, + process.env as platform.IProcessEnvironment, // Environment used for `findExecutable`. + false, // windowsEnableConpty: boolean, + logService, + ); + + // The current pid and title aren't exposed so they have to be tracked. + this.process.onProcessReady((event) => { + this._pid = event.pid; + this._onEvent.fire({ + type: 'ready', + pid: event.pid, + cwd: event.cwd, + }); + }); + + this.process.onProcessTitleChanged((title) => { + this._title = title; + this._onEvent.fire({ + type: 'titleChanged', + title, + }); + }); + + this.process.onProcessExit((exitCode) => { + logger.debug('Terminal exited', field('id', this.id), field('code', exitCode)); + this._onEvent.fire({ + type: 'exit', + exitCode, + }); + this.dispose(); + }); + + // TODO: I think `execCommand` must have something to do with running + // commands on the terminal that will do things in VS Code but we already + // have that functionality via a socket so I'm not sure what this is for. + // type: 'execCommand'; + // reqId: number; + // commandId: string; + // commandArgs: any[]; + + // TODO: Maybe this is to ask if the terminal is currently attached to + // anything? But we already know that on account of whether anything is + // listening to our event emitter. + // type: 'orphan?'; + } + + public dispose() { + logger.debug('Terminal disposing', field('id', this.id)); + this._onEvent.dispose(); + this.bufferer.dispose(); + this.process.dispose(); + this.process.shutdown(true); + this._onDispose.fire(); + this._onDispose.dispose(); + } + + public shutdown(immediate: boolean): void { + return this.process.shutdown(immediate); + } + + public getCwd(): Promise { + return this.process.getCwd(); + } + + public getInitialCwd(): Promise { + return this.process.getInitialCwd(); + } + + public start(): Promise { + return this.process.start(); + } + + public input(data: string): void { + return this.process.input(data); + } + + public resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + return this.process.resize(cols, rows); + } +} + +// References: - ../../workbench/api/node/extHostTerminalService.ts +// - ../../workbench/contrib/terminal/browser/terminalProcessManager.ts +export class TerminalProviderChannel implements IServerChannel, IDisposable { + private readonly terminals = new Map(); + private id = 0; + + public constructor (private readonly logService: ILogService) { + + } + + public listen(_: RemoteAgentConnectionContext, event: string, args?: any): Event { + switch (event) { + case '$onTerminalProcessEvent': return this.onTerminalProcessEvent(args); + } + + throw new Error(`Invalid listen '${event}'`); + } + + private onTerminalProcessEvent(args: terminal.IOnTerminalProcessEventArguments): Event { + return this.getTerminal(args.id).onEvent; + } + + public call(context: RemoteAgentConnectionContext, command: string, args?: any): Promise { + switch (command) { + case '$createTerminalProcess': return this.createTerminalProcess(context.remoteAuthority, args); + case '$startTerminalProcess': return this.startTerminalProcess(args); + case '$sendInputToTerminalProcess': return this.sendInputToTerminalProcess(args); + case '$shutdownTerminalProcess': return this.shutdownTerminalProcess(args); + case '$resizeTerminalProcess': return this.resizeTerminalProcess(args); + case '$getTerminalInitialCwd': return this.getTerminalInitialCwd(args); + case '$getTerminalCwd': return this.getTerminalCwd(args); + case '$sendCommandResultToTerminalProcess': return this.sendCommandResultToTerminalProcess(args); + case '$orphanQuestionReply': return this.orphanQuestionReply(args[0]); + case '$listTerminals': return this.listTerminals(args[0]); + } + + throw new Error(`Invalid call '${command}'`); + } + + public dispose(): void { + this.terminals.forEach((t) => t.dispose()); + } + + private async createTerminalProcess(remoteAuthority: string, args: terminal.ICreateTerminalProcessArguments): Promise { + const terminalId = this.id++; + logger.debug('Creating terminal', field('id', terminalId), field("terminals", this.terminals.size)); + + const shellLaunchConfig: IShellLaunchConfig = { + name: args.shellLaunchConfig.name, + executable: args.shellLaunchConfig.executable, + args: args.shellLaunchConfig.args, + // TODO: Should we transform if it's a string as well? The incoming + // transform only takes `UriComponents` so I suspect it's not necessary. + cwd: typeof args.shellLaunchConfig.cwd !== "string" + ? transformIncoming(remoteAuthority, args.shellLaunchConfig.cwd) + : args.shellLaunchConfig.cwd, + env: args.shellLaunchConfig.env, + }; + + const activeWorkspaceUri = transformIncoming(remoteAuthority, args.activeWorkspaceFolder?.uri); + const activeWorkspace = activeWorkspaceUri && args.activeWorkspaceFolder ? { + ...args.activeWorkspaceFolder, + uri: activeWorkspaceUri, + toResource: (relativePath: string) => resources.joinPath(activeWorkspaceUri, relativePath), + } : undefined; + + const resolverService = new VariableResolverService(remoteAuthority, args, process.env as platform.IProcessEnvironment); + const resolver = terminalEnvironment.createVariableResolver(activeWorkspace, resolverService); + + const getDefaultShellAndArgs = (): { executable: string; args: string[] | string } => { + if (shellLaunchConfig.executable) { + const executable = resolverService.resolve(activeWorkspace, shellLaunchConfig.executable); + let resolvedArgs: string[] | string = []; + if (shellLaunchConfig.args && Array.isArray(shellLaunchConfig.args)) { + for (const arg of shellLaunchConfig.args) { + resolvedArgs.push(resolverService.resolve(activeWorkspace, arg)); + } + } else if (shellLaunchConfig.args) { + resolvedArgs = resolverService.resolve(activeWorkspace, shellLaunchConfig.args); + } + return { executable, args: resolvedArgs }; + } + + const executable = terminalEnvironment.getDefaultShell( + (key) => args.configuration[key], + args.isWorkspaceShellAllowed, + getSystemShell(platform.platform), + process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), + process.env.windir, + resolver, + this.logService, + false, // useAutomationShell + ); + + const resolvedArgs = terminalEnvironment.getDefaultShellArgs( + (key) => args.configuration[key], + args.isWorkspaceShellAllowed, + false, // useAutomationShell + resolver, + this.logService, + ); + + return { executable, args: resolvedArgs }; + }; + + const getInitialCwd = (): string => { + return terminalEnvironment.getCwd( + shellLaunchConfig, + os.homedir(), + resolver, + activeWorkspaceUri, + args.configuration['terminal.integrated.cwd'], + this.logService, + ); + }; + + // Use a separate var so Typescript recognizes these properties are no + // longer undefined. + const resolvedShellLaunchConfig = { + ...shellLaunchConfig, + ...getDefaultShellAndArgs(), + cwd: getInitialCwd(), + }; + + logger.debug('Resolved shell launch configuration', field('id', terminalId)); + + // Use instead of `terminal.integrated.env.${platform}` to make types work. + const getEnvFromConfig = (): terminal.ISingleTerminalConfiguration => { + if (platform.isWindows) { + return args.configuration['terminal.integrated.env.windows']; + } else if (platform.isMacintosh) { + return args.configuration['terminal.integrated.env.osx']; + } + return args.configuration['terminal.integrated.env.linux']; + }; + + const getNonInheritedEnv = async (): Promise => { + const env = await getMainProcessParentEnv(); + env.VSCODE_IPC_HOOK_CLI = process.env['VSCODE_IPC_HOOK_CLI']!; + return env; + }; + + const env = terminalEnvironment.createTerminalEnvironment( + shellLaunchConfig, + getEnvFromConfig(), + resolver, + args.isWorkspaceShellAllowed, + product.version, + args.configuration['terminal.integrated.detectLocale'], + args.configuration['terminal.integrated.inheritEnv'] !== false + ? process.env as platform.IProcessEnvironment + : await getNonInheritedEnv() + ); + + // Apply extension environment variable collections to the environment. + if (!shellLaunchConfig.strictEnv) { + // They come in an array and in serialized format. + const envVariableCollections = new Map(); + for (const [k, v] of args.envVariableCollections) { + envVariableCollections.set(k, { map: deserializeEnvironmentVariableCollection(v) }); + } + const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); + mergedCollection.applyToProcessEnvironment(env); + } + + logger.debug('Resolved terminal environment', field('id', terminalId)); + + const terminal = new Terminal(terminalId, resolvedShellLaunchConfig, args, env, this.logService); + this.terminals.set(terminalId, terminal); + logger.debug('Created terminal', field('id', terminalId)); + terminal.onDispose(() => this.terminals.delete(terminalId)); + + return { + terminalId, + resolvedShellLaunchConfig, + }; + } + + private getTerminal(id: number): Terminal { + const terminal = this.terminals.get(id); + if (!terminal) { + throw new Error(`terminal with id ${id} does not exist`); + } + return terminal; + } + + private async startTerminalProcess(args: terminal.IStartTerminalProcessArguments): Promise { + return this.getTerminal(args.id).start(); + } + + private async sendInputToTerminalProcess(args: terminal.ISendInputToTerminalProcessArguments): Promise { + return this.getTerminal(args.id).input(args.data); + } + + private async shutdownTerminalProcess(args: terminal.IShutdownTerminalProcessArguments): Promise { + return this.getTerminal(args.id).shutdown(args.immediate); + } + + private async resizeTerminalProcess(args: terminal.IResizeTerminalProcessArguments): Promise { + return this.getTerminal(args.id).resize(args.cols, args.rows); + } + + private async getTerminalInitialCwd(args: terminal.IGetTerminalInitialCwdArguments): Promise { + return this.getTerminal(args.id).getInitialCwd(); + } + + private async getTerminalCwd(args: terminal.IGetTerminalCwdArguments): Promise { + return this.getTerminal(args.id).getCwd(); + } + + private async sendCommandResultToTerminalProcess(_: terminal.ISendCommandResultToTerminalProcessArguments): Promise { + // NOTE: Not required unless we implement the `execCommand` event, see above. + throw new Error('not implemented'); + } + + private async orphanQuestionReply(_: terminal.IOrphanQuestionReplyArgs): Promise { + // NOTE: Not required unless we implement the `orphan?` event, see above. + throw new Error('not implemented'); + } + + private async listTerminals(_: terminal.IListTerminalsArgs): Promise { + // TODO: args.isInitialization. Maybe this is to have slightly different + // behavior when first listing terminals but I don't know what you'd want to + // do differently. Maybe it's to reset the terminal dispose timeouts or + // something like that, but why not do it each time you list? + return Promise.all(Array.from(this.terminals).map(async ([id, terminal]) => { + const cwd = await terminal.getCwd(); + return { + id, + pid: terminal.pid, + title: terminal.title, + cwd, + workspaceId: terminal.workspaceId, + workspaceName: terminal.workspaceName, + }; + })); + } +} + +function transformIncoming(remoteAuthority: string, uri: UriComponents | undefined): URI | undefined { + const transformer = getUriTransformer(remoteAuthority); + return uri ? URI.revive(transformer.transformIncoming(uri)) : uri; +} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 index 0000000000000000000000000000000000000000..93062cadc627c61e0829c27a72894b81e6a0e039 --- /dev/null +++ b/src/vs/server/node/connection.ts @@ -0,0 +1,171 @@ +import { field, Logger, logger } from '@coder/logger'; +import * as cp from 'child_process'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; +import { FileAccess } from 'vs/base/common/network'; +import { ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; + +export abstract class Connection { + private readonly _onClose = new Emitter(); + public readonly onClose = this._onClose.event; + private disposed = false; + private _offline: number | undefined; + + public constructor(protected protocol: Protocol, public readonly token: string) {} + + public get offline(): number | undefined { + return this._offline; + } + + public reconnect(socket: ISocket, buffer: VSBuffer): void { + this._offline = undefined; + this.doReconnect(socket, buffer); + } + + public dispose(): void { + if (!this.disposed) { + this.disposed = true; + this.doDispose(); + this._onClose.fire(); + } + } + + protected setOffline(): void { + if (!this._offline) { + this._offline = Date.now(); + } + } + + /** + * Set up the connection on a new socket. + */ + protected abstract doReconnect(socket: ISocket, buffer: VSBuffer): void; + protected abstract doDispose(): void; +} + +/** + * Used for all the IPC channels. + */ +export class ManagementConnection extends Connection { + public constructor(protected protocol: Protocol, token: string) { + super(protocol, token); + protocol.onClose(() => this.dispose()); // Explicit close. + protocol.onSocketClose(() => this.setOffline()); // Might reconnect. + } + + protected doDispose(): void { + this.protocol.sendDisconnect(); + this.protocol.dispose(); + this.protocol.getUnderlyingSocket().destroy(); + } + + protected doReconnect(socket: ISocket, buffer: VSBuffer): void { + this.protocol.beginAcceptReconnection(socket, buffer); + this.protocol.endAcceptReconnection(); + } +} + +export class ExtensionHostConnection extends Connection { + private process?: cp.ChildProcess; + private readonly logger: Logger; + + public constructor( + locale:string, protocol: Protocol, buffer: VSBuffer, token: string, + private readonly environment: INativeEnvironmentService, + ) { + super(protocol, token); + this.logger = logger.named("exthost", field("token", token)); + this.protocol.dispose(); + this.spawn(locale, buffer).then((p) => this.process = p); + this.protocol.getUnderlyingSocket().pause(); + } + + protected doDispose(): void { + if (this.process) { + this.process.kill(); + } + this.protocol.getUnderlyingSocket().destroy(); + } + + protected doReconnect(socket: ISocket, buffer: VSBuffer): void { + // This is just to set the new socket. + this.protocol.beginAcceptReconnection(socket, null); + this.protocol.dispose(); + this.sendInitMessage(buffer); + } + + private sendInitMessage(buffer: VSBuffer): void { + const socket = this.protocol.getUnderlyingSocket(); + socket.pause(); + this.logger.trace('Sending socket'); + this.process!.send({ // Process must be set at this point. + type: 'VSCODE_EXTHOST_IPC_SOCKET', + initialDataChunk: (buffer.buffer as Buffer).toString('base64'), + skipWebSocketFrames: this.protocol.getSocket() instanceof NodeSocket, + }, socket); + } + + private async spawn(locale: string, buffer: VSBuffer): Promise { + this.logger.trace('Getting NLS configuration...'); + const config = await getNlsConfiguration(locale, this.environment.userDataPath); + this.logger.trace('Spawning extension host...'); + const proc = cp.fork( + FileAccess.asFileUri('bootstrap-fork', require).fsPath, + [ '--type=extensionHost' ], + { + env: { + ...process.env, + AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess', + PIPE_LOGGING: 'true', + VERBOSE_LOGGING: 'true', + VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true', + VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true', + VSCODE_LOG_STACK: 'false', + VSCODE_LOG_LEVEL: process.env.LOG_LEVEL, + VSCODE_NLS_CONFIG: JSON.stringify(config), + }, + silent: true, + }, + ); + + proc.on('error', (error) => { + this.logger.error('Exited unexpectedly', field('error', error)); + this.dispose(); + }); + proc.on('exit', (code) => { + this.logger.trace('Exited', field('code', code)); + this.dispose(); + }); + if (proc.stdout && proc.stderr) { + proc.stdout.setEncoding('utf8').on('data', (d) => this.logger.info(d)); + proc.stderr.setEncoding('utf8').on('data', (d) => this.logger.error(d)); + } + + proc.on('message', (event) => { + switch (event && event.type) { + case '__$console': + const severity = (this.logger)[event.severity] || 'info'; + (this.logger)[severity]('console', field('arguments', event.arguments)); + break; + case 'VSCODE_EXTHOST_DISCONNECTED': + this.logger.trace('Going offline'); + this.setOffline(); + break; + case 'VSCODE_EXTHOST_IPC_READY': + this.logger.trace('Got ready message'); + this.sendInitMessage(buffer); + break; + default: + this.logger.error('Unexpected message', field("event", event)); + break; + } + }); + + this.logger.trace('Waiting for handshake...'); + return proc; + } +} diff --git a/src/vs/server/node/insights.ts b/src/vs/server/node/insights.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0ece345f28f06afb2af12fe4901ad228b2475a4 --- /dev/null +++ b/src/vs/server/node/insights.ts @@ -0,0 +1,124 @@ +import * as appInsights from 'applicationinsights'; +import * as https from 'https'; +import * as http from 'http'; +import * as os from 'os'; + +class Channel { + public get _sender() { + throw new Error('unimplemented'); + } + public get _buffer() { + throw new Error('unimplemented'); + } + + public setUseDiskRetryCaching(): void { + throw new Error('unimplemented'); + } + public send(): void { + throw new Error('unimplemented'); + } + public triggerSend(): void { + throw new Error('unimplemented'); + } +} + +export class TelemetryClient { + public context: any = undefined; + public commonProperties: any = undefined; + public config: any = {}; + + public channel: any = new Channel(); + + public addTelemetryProcessor(): void { + throw new Error('unimplemented'); + } + + public clearTelemetryProcessors(): void { + throw new Error('unimplemented'); + } + + public runTelemetryProcessors(): void { + throw new Error('unimplemented'); + } + + public trackTrace(): void { + throw new Error('unimplemented'); + } + + public trackMetric(): void { + throw new Error('unimplemented'); + } + + public trackException(): void { + throw new Error('unimplemented'); + } + + public trackRequest(): void { + throw new Error('unimplemented'); + } + + public trackDependency(): void { + throw new Error('unimplemented'); + } + + public track(): void { + throw new Error('unimplemented'); + } + + public trackNodeHttpRequestSync(): void { + throw new Error('unimplemented'); + } + + public trackNodeHttpRequest(): void { + throw new Error('unimplemented'); + } + + public trackNodeHttpDependency(): void { + throw new Error('unimplemented'); + } + + public trackEvent(options: appInsights.Contracts.EventTelemetry): void { + if (!options.properties) { + options.properties = {}; + } + if (!options.measurements) { + options.measurements = {}; + } + + try { + const cpus = os.cpus(); + options.measurements.cores = cpus.length; + options.properties['common.cpuModel'] = cpus[0].model; + } catch (error) {} + + try { + options.measurements.memoryFree = os.freemem(); + options.measurements.memoryTotal = os.totalmem(); + } catch (error) {} + + try { + options.properties['common.shell'] = os.userInfo().shell; + options.properties['common.release'] = os.release(); + options.properties['common.arch'] = os.arch(); + } catch (error) {} + + try { + const url = process.env.TELEMETRY_URL || 'https://v1.telemetry.coder.com/track'; + const request = (/^http:/.test(url) ? http : https).request(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + request.on('error', () => { /* We don't care. */ }); + request.write(JSON.stringify(options)); + request.end(); + } catch (error) {} + } + + public flush(options: { callback: (v: string) => void }): void { + if (options.callback) { + options.callback(''); + } + } +} diff --git a/src/vs/server/node/ipc.ts b/src/vs/server/node/ipc.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e560eb46e6a0a18c91e440c655ac0d44b09b6dd --- /dev/null +++ b/src/vs/server/node/ipc.ts @@ -0,0 +1,61 @@ +import * as cp from 'child_process'; +import { Emitter } from 'vs/base/common/event'; + +enum ControlMessage { + okToChild = 'ok>', + okFromChild = 'ok<', +} + +interface RelaunchMessage { + type: 'relaunch'; + version: string; +} + +export type Message = RelaunchMessage; + +class IpcMain { + protected readonly _onMessage = new Emitter(); + public readonly onMessage = this._onMessage.event; + + public handshake(child?: cp.ChildProcess): Promise { + return new Promise((resolve, reject) => { + const target = child || process; + if (!target.send) { + throw new Error('Not spawned with IPC enabled'); + } + target.on('message', (message) => { + if (message === child ? ControlMessage.okFromChild : ControlMessage.okToChild) { + target.removeAllListeners(); + target.on('message', (msg) => this._onMessage.fire(msg)); + if (child) { + target.send!(ControlMessage.okToChild); + } + resolve(); + } + }); + if (child) { + child.once('error', reject); + child.once('exit', (code) => { + const error = new Error(`Unexpected exit with code ${code}`); + (error as any).code = code; + reject(error); + }); + } else { + target.send(ControlMessage.okFromChild); + } + }); + } + + public relaunch(version: string): void { + this.send({ type: 'relaunch', version }); + } + + private send(message: Message): void { + if (!process.send) { + throw new Error('Not a child process with IPC enabled'); + } + process.send(message); + } +} + +export const ipcMain = new IpcMain(); diff --git a/src/vs/server/node/logger.ts b/src/vs/server/node/logger.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a39c524aaa1b4031e04a631842f30b6fec3d98a --- /dev/null +++ b/src/vs/server/node/logger.ts @@ -0,0 +1,2 @@ +import { logger as baseLogger } from '@coder/logger'; +export const logger = baseLogger.named('vscode'); diff --git a/src/vs/server/node/marketplace.ts b/src/vs/server/node/marketplace.ts new file mode 100644 index 0000000000000000000000000000000000000000..8956fc40d48448b9932036c4c286464881807338 --- /dev/null +++ b/src/vs/server/node/marketplace.ts @@ -0,0 +1,174 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as tarStream from 'tar-stream'; +import * as util from 'util'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { mkdirp } from 'vs/base/node/pfs'; +import * as vszip from 'vs/base/node/zip'; +import * as nls from 'vs/nls'; +import product from 'vs/platform/product/common/product'; + +// We will be overriding these, so keep a reference to the original. +const vszipExtract = vszip.extract; +const vszipBuffer = vszip.buffer; + +export interface IExtractOptions { + overwrite?: boolean; + /** + * Source path within the TAR/ZIP archive. Only the files + * contained in this path will be extracted. + */ + sourcePath?: string; +} + +export interface IFile { + path: string; + contents?: Buffer | string; + localPath?: string; +} + +export const tar = async (tarPath: string, files: IFile[]): Promise => { + const pack = tarStream.pack(); + const chunks: Buffer[] = []; + const ended = new Promise((resolve) => { + pack.on('end', () => resolve(Buffer.concat(chunks))); + }); + pack.on('data', (chunk: Buffer) => chunks.push(chunk)); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + pack.entry({ name: file.path }, file.contents); + } + pack.finalize(); + await util.promisify(fs.writeFile)(tarPath, await ended); + return tarPath; +}; + +export const extract = async (archivePath: string, extractPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise => { + try { + await extractTar(archivePath, extractPath, options, token); + } catch (error) { + if (error.toString().includes('Invalid tar header')) { + await vszipExtract(archivePath, extractPath, options, token); + } + } +}; + +export const buffer = (targetPath: string, filePath: string): Promise => { + return new Promise(async (resolve, reject) => { + try { + let done: boolean = false; + await extractAssets(targetPath, new RegExp(filePath), (assetPath: string, data: Buffer) => { + if (path.normalize(assetPath) === path.normalize(filePath)) { + done = true; + resolve(data); + } + }); + if (!done) { + throw new Error('couldn\'t find asset ' + filePath); + } + } catch (error) { + if (error.toString().includes('Invalid tar header')) { + vszipBuffer(targetPath, filePath).then(resolve).catch(reject); + } else { + reject(error); + } + } + }); +}; + +const extractAssets = async (tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise => { + return new Promise((resolve, reject): void => { + const extractor = tarStream.extract(); + const fail = (error: Error) => { + extractor.destroy(); + reject(error); + }; + extractor.once('error', fail); + extractor.on('entry', async (header, stream, next) => { + const name = header.name; + if (match.test(name)) { + extractData(stream).then((data) => { + callback(name, data); + next(); + }).catch(fail); + } else { + stream.on('end', () => next()); + stream.resume(); // Just drain it. + } + }); + extractor.on('finish', resolve); + fs.createReadStream(tarPath).pipe(extractor); + }); +}; + +const extractData = (stream: NodeJS.ReadableStream): Promise => { + return new Promise((resolve, reject): void => { + const fileData: Buffer[] = []; + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(fileData))); + stream.on('data', (data) => fileData.push(data)); + }); +}; + +const extractTar = async (tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise => { + return new Promise((resolve, reject): void => { + const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : ''); + const extractor = tarStream.extract(); + const fail = (error: Error) => { + extractor.destroy(); + reject(error); + }; + extractor.once('error', fail); + extractor.on('entry', async (header, stream, next) => { + const nextEntry = (): void => { + stream.on('end', () => next()); + stream.resume(); + }; + + const rawName = path.normalize(header.name); + if (token.isCancellationRequested || !sourcePathRegex.test(rawName)) { + return nextEntry(); + } + + const fileName = rawName.replace(sourcePathRegex, ''); + const targetFileName = path.join(targetPath, fileName); + if (/\/$/.test(fileName)) { + return mkdirp(targetFileName).then(nextEntry); + } + + const dirName = path.dirname(fileName); + const targetDirName = path.join(targetPath, dirName); + if (targetDirName.indexOf(targetPath) !== 0) { + return fail(new Error(nls.localize('invalid file', 'Error extracting {0}. Invalid file.', fileName))); + } + + await mkdirp(targetDirName, undefined); + + const fstream = fs.createWriteStream(targetFileName, { mode: header.mode }); + fstream.once('close', () => next()); + fstream.once('error', fail); + stream.pipe(fstream); + }); + extractor.once('finish', resolve); + fs.createReadStream(tarPath).pipe(extractor); + }); +}; + +/** + * Override original functionality so we can use a custom marketplace with + * either tars or zips. + */ +export const enableCustomMarketplace = (): void => { + (product).extensionsGallery = { // Use `any` to override readonly. + serviceUrl: process.env.SERVICE_URL || 'https://extensions.coder.com/api', + itemUrl: process.env.ITEM_URL || '', + controlUrl: '', + recommendationsUrl: '', + ...(product.extensionsGallery || {}), + }; + + const target = vszip as typeof vszip; + target.zip = tar; + target.extract = extract; + target.buffer = buffer; +}; diff --git a/src/vs/server/node/nls.ts b/src/vs/server/node/nls.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d428a57d31f29c40f9c3ce45f715b443badf4e9 --- /dev/null +++ b/src/vs/server/node/nls.ts @@ -0,0 +1,88 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import * as lp from 'vs/base/node/languagePacks'; +import product from 'vs/platform/product/common/product'; +import { Translations } from 'vs/workbench/services/extensions/common/extensionPoints'; + +const configurations = new Map>(); +const metadataPath = path.join(getPathFromAmdModule(require, ''), 'nls.metadata.json'); + +export const isInternalConfiguration = (config: lp.NLSConfiguration): config is lp.InternalNLSConfiguration => { + return config && !!(config)._languagePackId; +}; + +const DefaultConfiguration = { + locale: 'en', + availableLanguages: {}, +}; + +export const getNlsConfiguration = async (locale: string, userDataPath: string): Promise => { + const id = `${locale}: ${userDataPath}`; + if (!configurations.has(id)) { + configurations.set(id, new Promise(async (resolve) => { + const config = product.commit && await util.promisify(fs.exists)(metadataPath) + ? await lp.getNLSConfiguration(product.commit, userDataPath, metadataPath, locale) + : DefaultConfiguration; + if (isInternalConfiguration(config)) { + config._languagePackSupport = true; + } + // If the configuration has no results keep trying since code-server + // doesn't restart when a language is installed so this result would + // persist (the plugin might not be installed yet or something). + if (config.locale !== 'en' && config.locale !== 'en-us' && Object.keys(config.availableLanguages).length === 0) { + configurations.delete(id); + } + resolve(config); + })); + } + return configurations.get(id)!; +}; + +export const getTranslations = async (locale: string, userDataPath: string): Promise => { + const config = await getNlsConfiguration(locale, userDataPath); + if (isInternalConfiguration(config)) { + try { + return JSON.parse(await util.promisify(fs.readFile)(config._translationsConfigFile, 'utf8')); + } catch (error) { /* Nothing yet. */} + } + return {}; +}; + +export const getLocaleFromConfig = async (userDataPath: string): Promise => { + const files = ['locale.json', 'argv.json']; + for (let i = 0; i < files.length; ++i) { + try { + const localeConfigUri = path.join(userDataPath, 'User', files[i]); + const content = stripComments(await util.promisify(fs.readFile)(localeConfigUri, 'utf8')); + return JSON.parse(content).locale; + } catch (error) { /* Ignore. */ } + } + return 'en'; +}; + +// Taken from src/main.js in the main VS Code source. +const stripComments = (content: string): string => { + const regexp = /('(?:[^\\']*(?:\\.)?)*')|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; + + return content.replace(regexp, (match, _m1, _m2, m3, m4) => { + // Only one of m1, m2, m3, m4 matches + if (m3) { + // A block comment. Replace with nothing + return ''; + } else if (m4) { + // A line comment. If it ends in \r?\n then keep it. + const length_1 = m4.length; + if (length_1 > 2 && m4[length_1 - 1] === '\n') { + return m4[length_1 - 2] === '\r' ? '\r\n' : '\n'; + } + else { + return ''; + } + } else { + // We match a string + return match; + } + }); +}; diff --git a/src/vs/server/node/protocol.ts b/src/vs/server/node/protocol.ts new file mode 100644 index 0000000000000000000000000000000000000000..0d9310038c0ca378579652d89bc8ac84924213db --- /dev/null +++ b/src/vs/server/node/protocol.ts @@ -0,0 +1,91 @@ +import { field } from '@coder/logger'; +import * as net from 'net'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from 'vs/platform/remote/common/remoteAgentConnection'; +import { logger } from 'vs/server/node/logger'; + +export interface SocketOptions { + readonly reconnectionToken: string; + readonly reconnection: boolean; + readonly skipWebSocketFrames: boolean; +} + +export class Protocol extends PersistentProtocol { + public constructor(socket: net.Socket, public readonly options: SocketOptions) { + super( + options.skipWebSocketFrames + ? new NodeSocket(socket) + : new WebSocketNodeSocket(new NodeSocket(socket)), + ); + } + + public getUnderlyingSocket(): net.Socket { + const socket = this.getSocket(); + return socket instanceof NodeSocket + ? socket.socket + : (socket as WebSocketNodeSocket).socket.socket; + } + + /** + * Perform a handshake to get a connection request. + */ + public handshake(): Promise { + logger.trace('Protocol handshake', field('token', this.options.reconnectionToken)); + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + logger.error('Handshake timed out', field('token', this.options.reconnectionToken)); + reject(new Error("timed out")); + }, 10000); // Matches the client timeout. + + const handler = this.onControlMessage((rawMessage) => { + try { + const raw = rawMessage.toString(); + logger.trace('Protocol message', field('token', this.options.reconnectionToken), field('message', raw)); + const message = JSON.parse(raw); + switch (message.type) { + case 'auth': + return this.authenticate(message); + case 'connectionType': + handler.dispose(); + clearTimeout(timeout); + return resolve(message); + default: + throw new Error('Unrecognized message type'); + } + } catch (error) { + handler.dispose(); + clearTimeout(timeout); + reject(error); + } + }); + + // Kick off the handshake in case we missed the client's opening shot. + // TODO: Investigate why that message seems to get lost. + this.authenticate(); + }); + } + + /** + * TODO: This ignores the authentication process entirely for now. + */ + private authenticate(_?: AuthRequest): void { + this.sendMessage({ type: 'sign', data: '' }); + } + + /** + * TODO: implement. + */ + public tunnel(): void { + throw new Error('Tunnel is not implemented yet'); + } + + /** + * Send a handshake message. In the case of the extension host, it just sends + * back a debug port. + */ + public sendMessage(message: HandshakeMessage | { debugPort?: number } ): void { + this.sendControl(VSBuffer.fromString(JSON.stringify(message))); + } +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 index 0000000000000000000000000000000000000000..c10a5a3a6771a94b2cbcb699bb1261051c71e08b --- /dev/null +++ b/src/vs/server/node/server.ts @@ -0,0 +1,302 @@ +import { field } from '@coder/logger'; +import * as fs from 'fs'; +import * as net from 'net'; +import * as path from 'path'; +import { Emitter } from 'vs/base/common/event'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { getMachineId } from 'vs/base/node/id'; +import { ClientConnectionEvent, createChannelReceiver, IPCServer, IServerChannel } from 'vs/base/parts/ipc/common/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/common/configurationService'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { getLogLevel, ILoggerService, ILogService } from 'vs/platform/log/common/log'; +import { LoggerChannel } from 'vs/platform/log/common/logIpc'; +import { LoggerService } from 'vs/platform/log/node/loggerService'; +import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ConnectionType, ConnectionTypeRequest } from 'vs/platform/remote/common/remoteAgentConnection'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { RequestChannel } from 'vs/platform/request/common/requestIpc'; +import { RequestService } from 'vs/platform/request/node/requestService'; +import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; +import { combinedAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +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, VscodeOptions, WorkbenchOptions } from 'vs/server/ipc'; +import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService, TerminalProviderChannel } from 'vs/server/node/channel'; +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'; +import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/workbench/services/remote/common/remoteAgentFileSystemChannel"; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; + +export class Vscode { + public readonly _onDidClientConnect = new Emitter(); + public readonly onDidClientConnect = this._onDidClientConnect.event; + private readonly ipc = new IPCServer(this.onDidClientConnect); + + private readonly maxExtraOfflineConnections = 0; + private readonly connections = new Map>(); + + private readonly services = new ServiceCollection(); + private servicesPromise?: Promise; + + public async cli(args: NativeParsedArgs): Promise { + return main(args); + } + + public async initialize(options: VscodeOptions): Promise { + const transformer = getUriTransformer(options.remoteAuthority); + if (!this.servicesPromise) { + this.servicesPromise = this.initializeServices(options.args); + } + await this.servicesPromise; + const environment = this.services.get(IEnvironmentService) as INativeEnvironmentService; + const startPath = options.startPath; + const parseUrl = (url: string): URI => { + // This might be a fully-specified URL or just a path. + try { + return URI.parse(url, true); + } catch (error) { + return URI.from({ + scheme: Schemas.vscodeRemote, + authority: options.remoteAuthority, + path: url, + }); + } + }; + return { + workbenchWebConfiguration: { + workspaceUri: startPath && startPath.workspace ? parseUrl(startPath.url) : undefined, + folderUri: startPath && !startPath.workspace ? parseUrl(startPath.url) : undefined, + remoteAuthority: options.remoteAuthority, + logLevel: getLogLevel(environment), + workspaceProvider: { + payload: [ + ["userDataPath", environment.userDataPath], + ["enableProposedApi", JSON.stringify(options.args["enable-proposed-api"] || [])] + ], + }, + }, + remoteUserDataUri: transformer.transformOutgoing(URI.file(environment.userDataPath)), + productConfiguration: product, + nlsConfiguration: await getNlsConfiguration(environment.args.locale || await getLocaleFromConfig(environment.userDataPath), environment.userDataPath), + commit: product.commit || 'development', + }; + } + + public async handleWebSocket(socket: net.Socket, query: Query): Promise { + if (!query.reconnectionToken) { + throw new Error('Reconnection token is missing from query parameters'); + } + const protocol = new Protocol(socket, { + reconnectionToken: query.reconnectionToken, + reconnection: query.reconnection === 'true', + skipWebSocketFrames: query.skipWebSocketFrames === 'true', + }); + try { + await this.connect(await protocol.handshake(), protocol); + } catch (error) { + protocol.sendMessage({ type: 'error', reason: error.message }); + protocol.dispose(); + protocol.getSocket().dispose(); + } + return true; + } + + private async connect(message: ConnectionTypeRequest, protocol: Protocol): Promise { + if (product.commit && message.commit !== product.commit) { + logger.warn(`Version mismatch (${message.commit} instead of ${product.commit})`); + } + + switch (message.desiredConnectionType) { + case ConnectionType.ExtensionHost: + case ConnectionType.Management: + if (!this.connections.has(message.desiredConnectionType)) { + this.connections.set(message.desiredConnectionType, new Map()); + } + const connections = this.connections.get(message.desiredConnectionType)!; + + const ok = async () => { + return message.desiredConnectionType === ConnectionType.ExtensionHost + ? { debugPort: await this.getDebugPort() } + : { type: 'ok' }; + }; + + const token = protocol.options.reconnectionToken; + if (protocol.options.reconnection && connections.has(token)) { + protocol.sendMessage(await ok()); + const buffer = protocol.readEntireBuffer(); + protocol.dispose(); + return connections.get(token)!.reconnect(protocol.getSocket(), buffer); + } else if (protocol.options.reconnection || connections.has(token)) { + throw new Error(protocol.options.reconnection + ? 'Unrecognized reconnection token' + : 'Duplicate reconnection token' + ); + } + + logger.debug('New connection', field('token', token)); + protocol.sendMessage(await ok()); + + let connection: Connection; + if (message.desiredConnectionType === ConnectionType.Management) { + connection = new ManagementConnection(protocol, token); + this._onDidClientConnect.fire({ + protocol, onDidClientDisconnect: connection.onClose, + }); + // TODO: Need a way to match clients with a connection. For now + // dispose everything which only works because no extensions currently + // utilize long-running proxies. + (this.services.get(INodeProxyService) as NodeProxyService)._onUp.fire(); + connection.onClose(() => (this.services.get(INodeProxyService) as NodeProxyService)._onDown.fire()); + } else { + const buffer = protocol.readEntireBuffer(); + connection = new ExtensionHostConnection( + message.args ? message.args.language : 'en', + protocol, buffer, token, + this.services.get(IEnvironmentService) as INativeEnvironmentService, + ); + } + connections.set(token, connection); + connection.onClose(() => { + logger.debug('Connection closed', field('token', token)); + connections.delete(token); + }); + this.disposeOldOfflineConnections(connections); + break; + case ConnectionType.Tunnel: return protocol.tunnel(); + default: throw new Error('Unrecognized connection type'); + } + } + + private disposeOldOfflineConnections(connections: Map): void { + const offline = Array.from(connections.values()) + .filter((connection) => typeof connection.offline !== 'undefined'); + for (let i = 0, max = offline.length - this.maxExtraOfflineConnections; i < max; ++i) { + logger.debug('Disposing offline connection', field("token", offline[i].token)); + offline[i].dispose(); + } + } + + private async initializeServices(args: NativeParsedArgs): Promise { + const environmentService = new NativeEnvironmentService(args); + // https://github.com/cdr/code-server/issues/1693 + fs.mkdirSync(environmentService.globalStorageHome.fsPath, { recursive: true }); + + const logService = new SpdLogService(RemoteExtensionLogFileName, environmentService.logsPath, getLogLevel(environmentService)); + const fileService = new FileService(logService); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(logService)); + + const piiPaths = [ + path.join(environmentService.userDataPath, 'clp'), // Language packs. + environmentService.appRoot, + environmentService.extensionsPath, + environmentService.builtinExtensionsPath, + ...environmentService.extraExtensionPaths, + ...environmentService.extraBuiltinExtensionPaths, + ]; + + this.ipc.registerChannel('logger', new LoggerChannel(logService)); + this.ipc.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel()); + + this.services.set(ILogService, logService); + this.services.set(IEnvironmentService, environmentService); + this.services.set(INativeEnvironmentService, environmentService); + this.services.set(ILoggerService, new SyncDescriptor(LoggerService)); + + const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); + await configurationService.initialize(); + this.services.set(IConfigurationService, configurationService); + + this.services.set(IRequestService, new SyncDescriptor(RequestService)); + this.services.set(IFileService, fileService); + this.services.set(IProductService, { _serviceBrand: undefined, ...product }); + + const machineId = await getMachineId(); + + await new Promise((resolve) => { + const instantiationService = new InstantiationService(this.services); + + instantiationService.invokeFunction((accessor) => { + instantiationService.createInstance(LogsDataCleaner); + + let telemetryService: ITelemetryService; + if (!environmentService.disableTelemetry) { + telemetryService = new TelemetryService({ + appender: combinedAppender( + new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any), + new TelemetryLogAppender(accessor.get(ILoggerService), environmentService) + ), + sendErrorTelemetry: true, + commonProperties: resolveCommonProperties( + product.commit, product.version, machineId, + [], environmentService.installSourcePath, 'code-server', + ), + piiPaths, + }, configurationService); + } else { + telemetryService = NullTelemetryService; + } + + this.services.set(ITelemetryService, telemetryService); + + this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + this.services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + this.services.set(INodeProxyService, new SyncDescriptor(NodeProxyService)); + + this.ipc.registerChannel('extensions', new ExtensionManagementChannel( + accessor.get(IExtensionManagementService), + (context) => getUriTransformer(context.remoteAuthority), + )); + this.ipc.registerChannel('remoteextensionsenvironment', new ExtensionEnvironmentChannel( + environmentService, logService, telemetryService, '', + )); + this.ipc.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); + this.ipc.registerChannel('telemetry', new TelemetryChannel(telemetryService)); + this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(accessor.get(INodeProxyService))); + this.ipc.registerChannel('localizations', >createChannelReceiver(accessor.get(ILocalizationsService))); + this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService)); + this.ipc.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new TerminalProviderChannel(logService)); + resolve(new ErrorTelemetry(telemetryService)); + }); + }); + } + + /** + * TODO: implement. + */ + private async getDebugPort(): Promise { + return undefined; + } +} diff --git a/src/vs/server/node/util.ts b/src/vs/server/node/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa47e993b46802f1a26457649e9e8bc467a73bf2 --- /dev/null +++ b/src/vs/server/node/util.ts @@ -0,0 +1,13 @@ +import { URITransformer } from 'vs/base/common/uriIpc'; + +export const getUriTransformer = (remoteAuthority: string): URITransformer => { + return new URITransformer(remoteAuthority); +}; + +/** + * Encode a path for opening via the folder or workspace query parameter. This + * preserves slashes so it can be edited by hand more easily. + */ +export const encodePath = (path: string): string => { + return path.split("/").map((p) => encodeURIComponent(p)).join("/"); +}; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index a4df8523631563a498c9ab6e51105074616a481a..f03da094e9080544102bbd3f037a71b348e5bd83 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -61,6 +61,7 @@ import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadTask'; import './mainThreadLabelService'; +import 'vs/server/browser/mainThreadNodeProxy'; import './mainThreadTunnelService'; import './mainThreadAuthentication'; import './mainThreadTimeline'; diff --git a/src/vs/workbench/api/browser/mainThreadStorage.ts b/src/vs/workbench/api/browser/mainThreadStorage.ts index 57abf0e86a5edeeb2bc497af5e140ec13d9b5810..704d0f9ae19d436a7207ff735aabc289c422dd1e 100644 --- a/src/vs/workbench/api/browser/mainThreadStorage.ts +++ b/src/vs/workbench/api/browser/mainThreadStorage.ts @@ -62,11 +62,11 @@ export class MainThreadStorage implements MainThreadStorageShape { return JSON.parse(jsonValue); } - $setValue(shared: boolean, key: string, value: object): Promise { + async $setValue(shared: boolean, key: string, value: object): Promise { let jsonValue: string; try { jsonValue = JSON.stringify(value); - this._storageService.store(key, jsonValue, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE); + await this._storageService.store(key, jsonValue, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE); } catch (err) { return Promise.reject(err); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 284c6aff854a747d1202c34581a1419c35e9654f..f0173d80103ca91b5eab144a10935bc0990119c9 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; @@ -103,6 +104,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStorage = accessor.get(IExtHostStorage); const extensionStoragePaths = accessor.get(IExtensionStoragePaths); const extHostLogService = accessor.get(ILogService); + const extHostNodeProxy = accessor.get(IExtHostNodeProxy); const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); @@ -114,6 +116,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); + rpcProtocol.set(ExtHostContext.ExtHostNodeProxy, extHostNodeProxy); rpcProtocol.set(ExtHostContext.ExtHostTunnelService, extHostTunnelService); rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 77ef6577821399b150407e980c8fd35e9d005ca6..264e3361accec20e4e1eaae10ae8ca05e47b1fae 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -816,6 +816,17 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } +export interface MainThreadNodeProxyShape extends IDisposable { + $send(message: string): void; + $fetchExtension(extensionUri: UriComponents): Promise; +} +export interface ExtHostNodeProxyShape { + $onMessage(message: string): void; + $onClose(): void; + $onDown(): void; + $onUp(): void; +} + export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; @@ -1796,6 +1807,7 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadNotebook: createMainId('MainThreadNotebook'), + MainThreadNodeProxy: createMainId('MainThreadNodeProxy'), MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') @@ -1838,6 +1850,7 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), + ExtHostNodeProxy: createMainId('ExtHostNodeProxy'), ExtHostTheming: createMainId('ExtHostTheming'), ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 328b9327207e4f2068bfab6cf374c622d8c5fc69..38963843095c9116011665027f46d3fb85c30ff8 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -31,6 +31,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { Emitter, Event } from 'vs/base/common/event'; @@ -82,6 +83,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected readonly _extHostWorkspace: ExtHostWorkspace; protected readonly _extHostConfiguration: ExtHostConfiguration; protected readonly _logService: ILogService; + protected readonly _nodeProxy: IExtHostNodeProxy; protected readonly _extHostTunnelService: IExtHostTunnelService; protected readonly _extHostTerminalService: IExtHostTerminalService; @@ -114,6 +116,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme @ILogService logService: ILogService, @IExtHostInitDataService initData: IExtHostInitDataService, @IExtensionStoragePaths storagePath: IExtensionStoragePaths, + @IExtHostNodeProxy nodeProxy: IExtHostNodeProxy, @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService, @IExtHostTerminalService extHostTerminalService: IExtHostTerminalService ) { @@ -125,6 +128,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._extHostWorkspace = extHostWorkspace; this._extHostConfiguration = extHostConfiguration; this._logService = logService; + this._nodeProxy = nodeProxy; this._extHostTunnelService = extHostTunnelService; this._extHostTerminalService = extHostTerminalService; this._disposables = new DisposableStore(); @@ -362,7 +366,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup); return Promise.all([ - this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder), + this._loadCommonJSModule(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder, !extensionDescription.browser), this._loadExtensionContext(extensionDescription) ]).then(values => { return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); @@ -754,7 +758,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; - protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; + protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, isRemote?: boolean): Promise; public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts index b3c89e51cfc25a53293a352a2a8ad50d5f26d595..e21abe4e13bc25a5b72f556bbfb61085842faeb7 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy'; +import { NotImplementedProxy } from 'vs/base/common/types'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtHostOutputService2 } from 'vs/workbench/api/node/extHostOutputService'; import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService'; @@ -36,3 +38,4 @@ registerSingleton(IExtHostSearch, NativeExtHostSearch); registerSingleton(IExtHostTask, ExtHostTask); registerSingleton(IExtHostTerminalService, ExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(String(IExtHostNodeProxy)) { whenReady = Promise.resolve(); }); diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index b3857616f7006127c423dcef7020ae4653da5ff6..1c1b80a2767bf77f30ca5bfee715c337120d3625 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -11,6 +11,8 @@ import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/ import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { ILogService } from 'vs/platform/log/common/log'; +import { join } from 'vs/base/common/path'; +import { tmpdir } from 'os'; export interface OpenCommandPipeArgs { type: 'open'; @@ -58,6 +60,11 @@ export class CLIServerBase { } private async setup(): Promise { + // NOTE@coder: Write this out so we can get the most recent path. + fs.promises.writeFile(join(tmpdir(), "vscode-ipc"), this._ipcHandlePath).catch((error) => { + this.logService.error(error); + }); + try { this._server.listen(this.ipcHandlePath); this._server.on('error', err => this.logService.error(err)); diff --git a/src/vs/workbench/api/worker/extHost.worker.services.ts b/src/vs/workbench/api/worker/extHost.worker.services.ts index 3843fdec386edc09a1d361b63de892a04e0070ed..8aac4df527857e964798362a69f5591bef07c165 100644 --- a/src/vs/workbench/api/worker/extHost.worker.services.ts +++ b/src/vs/workbench/api/worker/extHost.worker.services.ts @@ -8,6 +8,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostExtensionService } from 'vs/workbench/api/worker/extHostExtensionService'; import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService'; +import { ExtHostNodeProxy, IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy'; // ######################################################################### // ### ### @@ -17,3 +18,4 @@ import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService'; registerSingleton(IExtHostExtensionService, ExtHostExtensionService); registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostNodeProxy, ExtHostNodeProxy); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 021af6e0f8983c492f9cdd048ba2dcae7640bc1d..814dd0ff2fa7737e07833d8092c8f48953c73c47 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -11,6 +11,7 @@ import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterc import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { timeout } from 'vs/base/common/async'; +import { loadCommonJSModule } from 'vs/server/browser/worker'; class WorkerRequireInterceptor extends RequireInterceptor { @@ -46,10 +47,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { - return extensionDescription.browser; + // NOTE@coder: We can support regular Node modules as well. These will just + // require the root of the extension. + return extensionDescription.browser || "."; } - protected async _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { + protected async _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, isRemote?: boolean): Promise { + if (isRemote) { + return loadCommonJSModule(module, activationTimesBuilder, this._nodeProxy, this._logService, this._fakeModules!.getModule('vscode', module)); + } module = module.with({ path: ensureSuffix(module.path, '.js') }); const response = await fetch(module.toString(true)); diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba9e0762f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -55,6 +55,10 @@ align-items: center; justify-content: center; order: -1; + + /* NOTE@coder: Hide since it doesn't seem to do anything when used with + code-server except open the VS Code repository. */ + display: none !important; } .monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 80544aab34c12bb42a36519885e9872ef2b24158..17b56856a0b3fd936dbc094ff39797d5b8ccaadf 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -43,6 +43,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; import { toLocalISOString } from 'vs/base/common/date'; import { isWorkspaceToOpen, isFolderToOpen } from 'vs/platform/windows/common/windows'; import { getWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { initialize } from 'vs/server/browser/client'; import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -101,6 +102,8 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); + await initialize(services.serviceCollection); + // Return API Facade return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts index 94e7e7a4bac154c45078a1b5034e50634a7a43af..8164200dcef1efbc65b50eef9c270af3ca655fbd 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -15,6 +15,7 @@ import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { Schemas } from 'vs/base/common/network'; export class ResourceContextKey extends Disposable implements IContextKey { @@ -74,7 +75,8 @@ export class ResourceContextKey extends Disposable implements IContextKey { if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) { this._contextKeyService.bufferChangeEvents(() => { this._resourceKey.set(value); - this._schemeKey.set(value ? value.scheme : null); + // NOTE@coder: Fixes source control context menus (#1104). + this._schemeKey.set(value ? (value.scheme === Schemas.vscodeRemote ? Schemas.file : value.scheme) : null); this._filenameKey.set(value ? basename(value) : null); this._dirnameKey.set(value ? dirname(value).fsPath : null); this._pathKey.set(value ? value.fsPath : null); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 74f6922e98b4bb6a7fb100f5aac015afe9fc171b..3243a97c2d378013d96ffbe87e9df6dd4a66776d 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -149,9 +149,11 @@ margin-right: 8px; } -.scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { - flex-grow: 100; -} +/* NOTE@coder: Causes the label to shrink to zero width in Firefox due to + * overflow:hidden. This isn't right anyway, as far as I can tell. */ +/* .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { */ +/* flex-grow: 100; */ +/* } */ .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index ed4f26407391bd62219a9f8245a5cd63a7cb7488..92f26d1b082f80475cf76409a4569e948e9e0bd9 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -130,6 +130,8 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench extensionsPath?: string | undefined; extensionsDownloadPath: string = undefined!; builtinExtensionsPath: string = undefined!; + extraExtensionPaths: string[] = undefined!; + extraBuiltinExtensionPaths: string[] = undefined!; driverHandle?: string | undefined; diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts index 85d83f37da179a1e39266cf72a02e971f590308e..0659738b36df1747c9afcabf8d9abf26c890990b 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts @@ -125,11 +125,12 @@ export class DialogService implements IDialogService { async about(): Promise { const detailString = (useAgo: boolean): string => { return nls.localize('aboutDetail', - "Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}", + "code-server: v{4}\n VS Code: v{0}\nCommit: {1}\nDate: {2}\nBrowser: {3}", this.productService.version || 'Unknown', this.productService.commit || 'Unknown', this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown', - navigator.userAgent + navigator.userAgent, + this.productService.codeServerVersion || 'Unknown', ); }; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index a8d43045ecc8cbe04b3f8440cff16d42aadbcad0..8e122c761ac7ddfee11f9dda2ac5e845b893cc28 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -119,8 +119,25 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } + // NOTE@coder: Use the same path in // ../../../../platform/environment/node/environmentService.ts + // and don't use the user data scheme. This solves two problems: + // 1. Extensions running in the browser (like Vim) might use these paths + // directly instead of using the file service and most likely can't write + // to `/User` on disk. + // 2. Settings will be stored in the file system instead of in browser + // storage. Using browser storage makes sharing or seeding settings + // between browsers difficult. We may want to revisit this once/if we get + // settings sync. @memoize - get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.userData }); } + get userRoamingDataHome(): URI { return joinPath(URI.file(this.userDataPath).with({ scheme: Schemas.vscodeRemote }), 'User'); } + @memoize + get userDataPath(): string { + const dataPath = this.payload?.get("userDataPath"); + if (!dataPath) { + throw new Error("userDataPath was not provided to environment service"); + } + return dataPath; + } @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } @@ -301,7 +318,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); break; case 'enableProposedApi': - extensionHostDebugEnvironment.extensionEnabledProposedApi = []; + try { + extensionHostDebugEnvironment.extensionEnabledProposedApi = JSON.parse(value); + } catch (error) { + console.error(error); + extensionHostDebugEnvironment.extensionEnabledProposedApi = []; + } break; } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 50d4d812b76f09435fcff8148aac4ceeaeb30873..faacf88fcef119f9f959739656d64a84c8f64cbf 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -221,7 +221,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } } - return true; + return false; // NOTE@coder: Don't disable anything by extensionKind. } return false; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index de7e301d3f0c67ce662827f61427a5a7b3616b9f..877ea8e11e6e6d34b9a8fe16287af309e569285e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -251,7 +251,9 @@ export class ExtensionManagementService extends Disposable implements IWorkbench // Install Language pack on all servers if (isLanguagePackExtension(manifest)) { - servers.push(...this.servers); + // NOTE@coder: It does not appear language packs can be installed on the web + // extension management server at this time. Filter out the web to fix this. + servers.push(...this.servers.filter(s => s !== this.extensionManagementServerService.webExtensionManagementServer)); } else { const server = this.getExtensionManagementServerToInstall(manifest); if (server) { @@ -320,6 +322,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return this.extensionManagementServerService.webExtensionManagementServer; } + // NOTE@coder: Fall back to installing on the remote server. + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.extensionManagementServerService.remoteExtensionManagementServer; + } + return undefined; } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 1dff19bf177eff24f722b748b79835a653241c4d..0f59ad290c82cc4c9d09c565c1018cc275ca0249 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -177,8 +177,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); - localExtensions = this._checkEnabledAndProposedAPI(localExtensions); remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions); + // NOTE@coder: Include remotely hosted extensions that should run locally. + localExtensions = this._checkEnabledAndProposedAPI(localExtensions) + .concat(remoteExtensions.filter(ext => !ext.browser && ext.extensionKind && (ext.extensionKind === "web" || ext.extensionKind.includes("web")))); const remoteAgentConnection = this._remoteAgentService.getConnection(); this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 65e532ee58dfc06ed944846d01b885cb8f260ebc..0b6282fde7ad03c7ea9872a777cbf487253abed1 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -37,7 +37,8 @@ export function canExecuteOnWorkspace(manifest: IExtensionManifest, productServi export function canExecuteOnWeb(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean { const extensionKind = getExtensionKind(manifest, productService, configurationService); - return extensionKind.some(kind => kind === 'web'); + // NOTE@coder: Hardcode vim for now. + return extensionKind.some(kind => kind === 'web') || manifest.name === 'vim'; } export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] { diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts index e39d131fe7b1dd4bd1093fedb8faba8e1fe969e8..94f2f1d7c4a0b3cb46eaaffe1181b3abbf997d7f 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -16,7 +16,7 @@ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; import { MessageType, createMessageOfType, isMessageOfType, IExtHostSocketMessage, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionHostMain, IExitFn } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IURITransformer, URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; +import { IURITransformer, URITransformer } from 'vs/base/common/uriIpc'; import { exists } from 'vs/base/node/pfs'; import { realpath } from 'vs/base/node/extpath'; import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; @@ -57,12 +57,13 @@ const args = minimist(process.argv.slice(2), { const Module = require.__$__nodeRequire('module') as any; const originalLoad = Module._load; - Module._load = function (request: string) { + Module._load = function (request: string, parent: object, isMain: boolean) { if (request === 'natives') { throw new Error('Either the extension or a NPM dependency is using the "natives" node module which is unsupported as it can cause a crash of the extension host. Click [here](https://go.microsoft.com/fwlink/?linkid=871887) to find out more'); } - return originalLoad.apply(this, arguments); + // NOTE@coder: Map node_module.asar requests to regular node_modules. + return originalLoad.apply(this, [request.replace(/node_modules\.asar(\.unpacked)?/, 'node_modules'), parent, isMain]); }; })(); @@ -135,8 +136,11 @@ function _createExtHostProtocol(): Promise { // Wait for rich client to reconnect protocol.onSocketClose(() => { - // The socket has closed, let's give the renderer a certain amount of time to reconnect - disconnectRunner1.schedule(); + // NOTE@coder: Inform the server so we can manage offline + // connections there instead. Our goal is to persist connections + // forever (to a reasonable point) to account for things like + // hibernating overnight. + process.send!({ type: 'VSCODE_EXTHOST_DISCONNECTED' }); }); } } @@ -313,11 +317,9 @@ export async function startExtensionHostProcess(): Promise { // Attempt to load uri transformer let uriTransformer: IURITransformer | null = null; - if (initData.remote.authority && args.uriTransformerPath) { + if (initData.remote.authority) { try { - const rawURITransformerFactory = require.__$__nodeRequire(args.uriTransformerPath); - const rawURITransformer = rawURITransformerFactory(initData.remote.authority); - uriTransformer = new URITransformer(rawURITransformer); + uriTransformer = new URITransformer(initData.remote.authority); } catch (e) { console.error(e); } diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts index b39a5cbb9eadbc046144d2e76d26a9b0e950ddaa..3b4cc7274e149ee10dba0dbbb09cf25939091f4b 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts @@ -15,7 +15,11 @@ require.config({ baseUrl: monacoBaseUrl, catchError: true, - createTrustedScriptURL: (value: string) => value + createTrustedScriptURL: (value: string) => value, + paths: { + '@coder/node-browser': `../node_modules/@coder/node-browser/out/client/client.js`, + '@coder/requirefs': `../node_modules/@coder/requirefs/out/requirefs.js`, + } }); require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err)); diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts index d7aefde89c74bc6096d6e66c45368c8582594efa..9758f3bb96b48603251336e6a64e270ee89744f0 100644 --- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts +++ b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts @@ -5,8 +5,8 @@ import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; -import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; // @ts-ignore: interface is implemented via proxy export class LocalizationsService implements ILocalizationsService { @@ -14,9 +14,9 @@ export class LocalizationsService implements ILocalizationsService { declare readonly _serviceBrand: undefined; constructor( - @ISharedProcessService sharedProcessService: ISharedProcessService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, ) { - return createChannelSender(sharedProcessService.getChannel('localizations')); + return createChannelSender(remoteAgentService.getConnection()!.getChannel('localizations')); } } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 509f8ac8ce3a689386e439302a53c27e4fdfcef7..2bf9a737bd0dbfa1e604acfc890be45823f02ebe 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -35,7 +35,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService'; import 'vs/workbench/services/keybinding/browser/keymapService'; import 'vs/workbench/services/extensions/browser/extensionService'; import 'vs/workbench/services/extensionManagement/common/extensionManagementServerService'; -import 'vs/workbench/services/telemetry/browser/telemetryService'; +// NOTE@coder: We send it all to the server side to be processed there instead. +// import 'vs/workbench/services/telemetry/browser/telemetryService'; import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/services/url/browser/urlService'; diff --git a/yarn.lock b/yarn.lock index ff358cb6a10984868ed5a5aed5729ac6eb8ebeb7..69668d95ecad219da26ccc4d837913b9324a0e28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,23 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@coder/logger@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.12.tgz#def113b7183abc35a8da2b57f0929f7e9626f4e0" + integrity sha512-oM0j3lTVPqApUm3e0bKKcXpfAiJEys31fgEfQlHmvEA13ujsC4zDuXnt0uzDtph48eMoNRLOF/EE4mNShVJKVw== + +"@coder/node-browser@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@coder/node-browser/-/node-browser-1.0.8.tgz#c22f581b089ad7d95ad1362fd351c57b7fbc6e70" + integrity sha512-NLF9sYMRCN9WK1C224pHax1Cay3qKypg25BhVg7VfNbo3Cpa3daata8RF/rT8JK3lPsu8PmFgDRQjzGC9X1Lrw== + +"@coder/requirefs@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@coder/requirefs/-/requirefs-1.1.5.tgz#259db370d563a79a96fb150bc9d69c7db6edc9fb" + integrity sha512-3jB47OFCql9+9FI6Vc4YX0cfFnG5rxBfrZUH45S4XYtYGOz+/Xl4h4d2iMk50b7veHkeSWGlB4VHC3UZ16zuYQ== + optionalDependencies: + jszip "2.6.0" + "@electron/get@^1.0.1": version "1.7.2" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd" @@ -5403,6 +5420,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +jszip@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-2.6.0.tgz#7fb3e9c2f11c8a9840612db5dabbc8cf3a7534b7" + integrity sha1-f7PpwvEciphAYS212rvIzzp1NLc= + dependencies: + pako "~1.0.0" + just-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" @@ -5983,26 +6007,11 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@^1.2.5: +minimist@0.0.8, minimist@^1.2.0, minimist@^1.2.5, minimist@~0.0.1: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - minipass@^2.2.1, minipass@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" @@ -6744,6 +6753,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== +pako@~1.0.0: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"