diff --git a/.gitignore b/.gitignore index 0fe46b6eadc..e545e004cef 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 135e10442a7..00000000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "https://atom.io/download/electron" -target "7.3.2" -runtime "electron" diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index f2ea1bd3701..3f660f99819 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -52,6 +52,7 @@ gulp.task('vscode-reh-web-linux-x64-min', noop); gulp.task('vscode-reh-web-linux-alpine-min', noop); 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/node.js b/build/lib/node.js index 403ae3d9657..738ee8cee0e 100644 --- a/build/lib/node.js +++ b/build/lib/node.js @@ -5,11 +5,8 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); -const fs = require("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); diff --git a/build/lib/node.ts b/build/lib/node.ts index 64397034461..c53dccf4dc0 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.js b/build/lib/util.js index e552a036f89..169e8614b9f 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -257,6 +257,7 @@ function streamToPromise(stream) { } exports.streamToPromise = streamToPromise; function getElectronVersion() { + 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/lib/util.ts b/build/lib/util.ts index 035c7e95ea3..4ff8dcfe6b2 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -322,6 +322,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 8f8b0019a77..ea054c725be 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 cb88d37adef..6b3253af0a3 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 00000000000..9cb693af63b --- /dev/null +++ b/coder.js @@ -0,0 +1,63 @@ +// 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.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 da4fa3e9d04..50f3e1144f8 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/package.json b/package.json index 226f51a1ec5..5c4e5af5f69 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,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.2.3", "graceful-fs": "4.2.3", @@ -59,6 +63,7 @@ "native-keymap": "2.1.2", "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta8", + "rimraf": "^2.2.8", "semver-umd": "^5.5.7", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", @@ -159,7 +164,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", @@ -190,5 +194,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 2b884d18f30..518b935b837 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 1e16cde724c..00000000000 --- a/remote/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "http://nodejs.org/dist" -target "12.4.0" -runtime "node" diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 1286c5117a4..e60dd11d039 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -111,16 +111,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 0bbc5d6ef91..61f139b9c55 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -59,6 +59,17 @@ 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. */ } + } } else if (typeof process === 'object') { _isWindows = (process.platform === 'win32'); _isMacintosh = (process.platform === 'darwin'); diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index c52f7b3774f..08a87fa970f 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -110,7 +110,8 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve /^ELECTRON_.+$/, /^GOOGLE_API_KEY$/, /^VSCODE_.+$/, - /^SNAP(|_.*)$/ + /^SNAP(|_.*)$/, + /^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 ef2291d49b1..29b2f9dfc2b 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 2c64061da7b..c0ef8faedd4 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 c629f7fffa1..c266e1fb06f 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -13,6 +13,8 @@ import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/wi import { isEqual } from 'vs/base/common/resources'; import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; +import { Schemas } from 'vs/base/common/network'; +import { encodePath } from 'vs/server/node/util'; interface ICredential { service: string; @@ -243,12 +245,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 @@ -285,7 +293,22 @@ class WorkspaceProvider implements IWorkspaceProvider { 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)) { @@ -297,40 +320,7 @@ class WorkspaceProvider implements IWorkspaceProvider { // 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/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 2379b626c81..28f8971cf39 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -8,6 +8,8 @@ import { localize } from 'vs/nls'; import { isWindows } from 'vs/base/common/platform'; export interface ParsedArgs { + '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 @@ -141,6 +143,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.") }, @@ -403,4 +407,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 5c0dc4ad4ae..38b8c7573a8 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -38,6 +38,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { extensionsPath?: string; extensionsDownloadPath: string; builtinExtensionsPath: string; + extraExtensionPaths: string[]; + extraBuiltinExtensionPaths: string[]; driverHandle?: string; driverVerbose: boolean; @@ -180,6 +182,13 @@ export class EnvironmentService 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 575b2aafc38..873181f9678 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -85,7 +85,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 { @@ -211,7 +211,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); } @@ -241,7 +247,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; } @@ -345,4 +351,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 3370a608b4b..37b3592d39d 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -30,6 +30,12 @@ if (isWeb) { ], }); } + // 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)); + } } // Node: AMD loader diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index 040c869d94c..bf16defcf7b 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -30,6 +30,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 3715cbb8e6e..c65de8ad37e 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 2185bb5228c..35463ca6520 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -89,7 +89,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 59b1baf912..cf9805554b 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -116,8 +116,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 1623957cb1..d366438d54 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -83,7 +83,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 75514fe5a4..62d97c6048 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -204,8 +204,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 00000000000..3c0703b7174 --- /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 00000000000..ed7c078077b --- /dev/null +++ b/src/vs/server/browser/extHostNodeProxy.ts @@ -0,0 +1,46 @@ +import { Emitter } from 'vs/base/common/event'; +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); + } +} + +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 00000000000..0d2e93edae2 --- /dev/null +++ b/src/vs/server/browser/mainThreadNodeProxy.ts @@ -0,0 +1,37 @@ +import { IDisposable } from 'vs/base/common/lifecycle'; +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); + } + } + + 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 00000000000..5ae44cdc856 --- /dev/null +++ b/src/vs/server/browser/worker.ts @@ -0,0 +1,56 @@ +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 fetchUri = URI.from({ + scheme: self.location.protocol.replace(':', ''), + authority: self.location.host, + path: self.location.pathname.replace(/\/static\/([^\/]+)\/.*$/, '/static/$1\/'), + query: `tar=${encodeURIComponent(module.path)}`, + }); + const response = await fetch(fetchUri.toString(true)); + if (response.status !== 200) { + throw new Error(`Failed to download extension "${module}"`); + } + const client = new Client(nodeProxy, { logger: logService }); + const init = await client.handshake(); + const buffer = new Uint8Array(await response.arrayBuffer()); + 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 00000000000..14b9de879ce --- /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 00000000000..4ea6d95d36a --- /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 00000000000..ab020fbb4e4 --- /dev/null +++ b/src/vs/server/entry.ts @@ -0,0 +1,78 @@ +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('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 00000000000..56331ff1fc3 --- /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 00000000000..33b28cf2d53 --- /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 }; + +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 00000000000..e10cc9c218b --- /dev/null +++ b/src/vs/server/node/channel.ts @@ -0,0 +1,360 @@ +import { Server } from '@coder/node-browser'; +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 { OS } from 'vs/base/common/platform'; +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/node/environmentService'; +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 { 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: 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); + } +} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 index 00000000000..36e80fb6966 --- /dev/null +++ b/src/vs/server/node/connection.ts @@ -0,0 +1,157 @@ +import * as cp from 'child_process'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; +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/node/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { getNlsConfiguration } from 'vs/server/node/nls'; +import { Protocol } from 'vs/server/node/protocol'; +import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; + +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; + + public constructor( + locale:string, protocol: Protocol, buffer: VSBuffer, token: string, + private readonly log: ILogService, + private readonly environment: INativeEnvironmentService, + ) { + super(protocol, 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.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 { + const config = await getNlsConfiguration(locale, this.environment.userDataPath); + const proc = cp.fork( + getPathFromAmdModule(require, 'bootstrap-fork'), + [ '--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', () => this.dispose()); + proc.on('exit', () => this.dispose()); + if (proc.stdout && proc.stderr) { + proc.stdout.setEncoding('utf8').on('data', (d) => this.log.info('Extension host stdout', d)); + proc.stderr.setEncoding('utf8').on('data', (d) => this.log.error('Extension host stderr', d)); + } + proc.on('message', (event) => { + if (event && event.type === '__$console') { + const severity = (this.log)[event.severity] ? event.severity : 'info'; + (this.log)[severity]('Extension host', event.arguments); + } + if (event && event.type === 'VSCODE_EXTHOST_DISCONNECTED') { + this.setOffline(); + } + }); + + const listen = (message: IExtHostReadyMessage) => { + if (message.type === 'VSCODE_EXTHOST_IPC_READY') { + proc.removeListener('message', listen); + this.sendInitMessage(buffer); + } + }; + + return proc.on('message', listen); + } +} diff --git a/src/vs/server/node/insights.ts b/src/vs/server/node/insights.ts new file mode 100644 index 00000000000..a0ece345f28 --- /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 00000000000..5e560eb46e6 --- /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 00000000000..2a39c524aaa --- /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 00000000000..8956fc40d48 --- /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 00000000000..3d428a57d31 --- /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 00000000000..3c74512192a --- /dev/null +++ b/src/vs/server/node/protocol.ts @@ -0,0 +1,73 @@ +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'; + +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 { + return new Promise((resolve, reject) => { + const handler = this.onControlMessage((rawMessage) => { + try { + const message = JSON.parse(rawMessage.toString()); + switch (message.type) { + case 'auth': return this.authenticate(message); + case 'connectionType': + handler.dispose(); + return resolve(message); + default: throw new Error('Unrecognized message type'); + } + } catch (error) { + handler.dispose(); + reject(error); + } + }); + }); + } + + /** + * TODO: This ignores the authentication process entirely for now. + */ + private authenticate(_message: 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 00000000000..4b88fedb2f0 --- /dev/null +++ b/src/vs/server/node/server.ts @@ -0,0 +1,285 @@ +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 { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ParsedArgs } from 'vs/platform/environment/node/argv'; +import { EnvironmentService, INativeEnvironmentService } 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, ILogService } from 'vs/platform/log/common/log'; +import { LoggerChannel } from 'vs/platform/log/common/logIpc'; +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 { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; +import { combinedAppender, LogAppender, 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 } 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_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: ParsedArgs): 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' + ); + } + + 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(ILogService) as ILogService, + this.services.get(IEnvironmentService) as INativeEnvironmentService, + ); + } + connections.set(token, connection); + connection.onClose(() => 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) { + offline[i].dispose(); + } + } + + private async initializeServices(args: ParsedArgs): Promise { + const environmentService = new EnvironmentService(args, process.execPath); + // 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); + + 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 }); + this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); + this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + + if (!environmentService.disableTelemetry) { + this.services.set(ITelemetryService, new TelemetryService({ + appender: combinedAppender( + new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any, logService), + new LogAppender(logService), + ), + sendErrorTelemetry: true, + commonProperties: resolveCommonProperties( + product.commit, product.version, await getMachineId(), + [], environmentService.installSourcePath, 'code-server', + ), + piiPaths, + }, configurationService)); + } else { + this.services.set(ITelemetryService, NullTelemetryService); + } + + await new Promise((resolve) => { + const instantiationService = new InstantiationService(this.services); + this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService)); + this.services.set(INodeProxyService, instantiationService.createInstance(NodeProxyService)); + + instantiationService.invokeFunction(() => { + instantiationService.createInstance(LogsDataCleaner); + const telemetryService = this.services.get(ITelemetryService) as ITelemetryService; + this.ipc.registerChannel('extensions', new ExtensionManagementChannel( + this.services.get(IExtensionManagementService) as IExtensionManagementService, + (context) => getUriTransformer(context.remoteAuthority), + )); + this.ipc.registerChannel('remoteextensionsenvironment', new ExtensionEnvironmentChannel( + environmentService, logService, telemetryService, '', + )); + this.ipc.registerChannel('request', new RequestChannel(this.services.get(IRequestService) as IRequestService)); + this.ipc.registerChannel('telemetry', new TelemetryChannel(telemetryService)); + this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(this.services.get(INodeProxyService) as INodeProxyService)); + this.ipc.registerChannel('localizations', >createChannelReceiver(this.services.get(ILocalizationsService) as ILocalizationsService)); + this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, 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 00000000000..fa47e993b46 --- /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 3d77009b908..11deb1b99ac 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -60,6 +60,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 7bc3904963..c6db2368ae 100644 --- a/src/vs/workbench/api/browser/mainThreadStorage.ts +++ b/src/vs/workbench/api/browser/mainThreadStorage.ts @@ -58,11 +58,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 97793666ad8..13cd137db1e 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'; @@ -97,6 +98,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); @@ -107,6 +109,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 eb5d8ea8455..da9eb521ca4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -769,6 +769,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } +export interface MainThreadNodeProxyShape extends IDisposable { + $send(message: string): void; +} +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; @@ -1707,6 +1717,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') @@ -1745,6 +1756,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 34639e18b6f..9c22fe6f090 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -32,6 +32,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(); @@ -355,7 +359,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 async $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 b3c89e51cfc..e21abe4e13b 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/worker/extHost.worker.services.ts b/src/vs/workbench/api/worker/extHost.worker.services.ts index 3843fdec386..8aac4df5278 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 c71ab1c7da4..572b07ff251 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -9,6 +9,7 @@ import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHost import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { loadCommonJSModule } from 'vs/server/browser/worker'; class WorkerRequireInterceptor extends RequireInterceptor { @@ -42,10 +43,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 ced2d815834..dfcae73e8a0 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 0462617196b..11434d27af9 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -45,6 +45,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 { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; @@ -84,6 +85,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 18ea0bfedb4..d59a17c17f4 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 { @@ -68,7 +69,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._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); this._extensionKey.set(value ? extname(value) : null); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 9947f240bf2..bdba0a2fc64 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -138,9 +138,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/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts index 6e3182a696d..7df85da165a 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts @@ -124,11 +124,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 ba2701ec54d..4d4aaa6958b 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -121,8 +121,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } + // NOTE@coder: Use the regular path for extensions that write directly to disk + // instead of using the VS Code API. @memoize - get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.userData }); } + get userRoamingDataHome(): URI { return URI.file(this.userDataPath).with({ scheme: Schemas.userData }); } + @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'); } @@ -284,7 +294,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/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts index c28b1477400..6090200d9c3 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts @@ -163,7 +163,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 33eb56db3c2..e5167794c3f 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -236,6 +236,11 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery); } + // NOTE@coder: Fall back to installing on the remote server. + if (this.extensionManagementServerService.remoteExtensionManagementServer) { + return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery); + } + if (this.extensionManagementServerService.remoteExtensionManagementServer) { const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index d0710e77fa2..ceb27174aee 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -116,8 +116,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.extensionKind && (ext.extensionKind === "web" || ext.extensionKind.includes("web")))); const remoteAgentConnection = this._remoteAgentService.getConnection(); this._runningLocation = _determineRunningLocation(this._productService, this._configService, localExtensions, remoteExtensions, Boolean(remoteEnv && remoteAgentConnection)); diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 65e532ee58d..0b6282fde7a 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 49542eda74c..de0e2da0a4c 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 } 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 79455414c06..a407593b4dc 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts @@ -14,7 +14,11 @@ require.config({ baseUrl: monacoBaseUrl, - catchError: true + catchError: true, + 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 44999bd842e..601b1c54088 100644 --- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts +++ b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts @@ -5,17 +5,17 @@ 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'; export class LocalizationsService { 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 0669178db4c..28fafeb2de2 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 b2fbf543af3..f10dddd6594 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" @@ -5421,6 +5438,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" @@ -6008,26 +6032,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" @@ -6797,6 +6806,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"