Archived
1
0
This repository has been archived on 2024-09-09. You can view files and clone it, but cannot push or open issues or pull requests.
code-server/ci/dev/vscode.patch
Asher 3d7fbec40f
Use file system for settings and fix data home path
It's possible that using browser storage makes more sense with settings
sync, so we might want to revisit this once/if we get settings sync
working. As it currently is though, browser storage just causes jank.

The path was also missing a `User` at the end so I added that. This
might affect the Vim extension which would have been writing to the
wrong path previously but I don't believe it should affect anything
else since they would have been writing to browser storage.

- Fixes #2208
- Fixes #2231
- Fixes #2279
- Fixes #2274
2020-11-20 14:03:07 -06:00

4110 lines
166 KiB
Diff

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