mirror of https://git.tuxpa.in/a/code-server.git
3219 lines
127 KiB
Diff
3219 lines
127 KiB
Diff
diff --git a/.gitignore b/.gitignore
|
|
index e73dd4d9e8..e3192b3a0d 100644
|
|
--- a/.gitignore
|
|
+++ b/.gitignore
|
|
@@ -24,7 +24,6 @@ out-vscode-reh-web-min/
|
|
out-vscode-reh-web-pkg/
|
|
out-vscode-web/
|
|
out-vscode-web-min/
|
|
-src/vs/server
|
|
resources/server
|
|
build/node_modules
|
|
coverage/
|
|
diff --git a/.yarnrc b/.yarnrc
|
|
index 7808166004..1e16cde724 100644
|
|
--- a/.yarnrc
|
|
+++ b/.yarnrc
|
|
@@ -1,3 +1,3 @@
|
|
-disturl "https://atom.io/download/electron"
|
|
-target "7.1.11"
|
|
-runtime "electron"
|
|
+disturl "http://nodejs.org/dist"
|
|
+target "12.4.0"
|
|
+runtime "node"
|
|
diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js
|
|
index 7a2320d828..5768890636 100644
|
|
--- a/build/npm/postinstall.js
|
|
+++ b/build/npm/postinstall.js
|
|
@@ -33,9 +33,9 @@ function yarnInstall(location, opts) {
|
|
|
|
yarnInstall('extensions'); // node modules shared by all extensions
|
|
|
|
-yarnInstall('remote'); // node modules used by vscode server
|
|
+// yarnInstall('remote'); // node modules used by vscode server
|
|
|
|
-yarnInstall('remote/web'); // node modules used by vscode web
|
|
+// yarnInstall('remote/web'); // node modules used by vscode web
|
|
|
|
const allExtensionFolders = fs.readdirSync('extensions');
|
|
const extensions = allExtensionFolders.filter(e => {
|
|
@@ -68,7 +68,7 @@ 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
|
|
diff --git a/coder.js b/coder.js
|
|
new file mode 100644
|
|
index 0000000000..6aee0e46bc
|
|
--- /dev/null
|
|
+++ b/coder.js
|
|
@@ -0,0 +1,70 @@
|
|
+// This must be ran from VS Code's root.
|
|
+const gulp = require("gulp");
|
|
+const path = require("path");
|
|
+const _ = require("underscore");
|
|
+const buildfile = require("./src/buildfile");
|
|
+const common = require("./build/lib/optimize");
|
|
+const util = require("./build/lib/util");
|
|
+const deps = require("./build/dependencies");
|
|
+
|
|
+const vscodeEntryPoints = _.flatten([
|
|
+ buildfile.entrypoint("vs/workbench/workbench.web.api"),
|
|
+ buildfile.entrypoint("vs/server/entry"),
|
|
+ buildfile.base,
|
|
+ buildfile.workbenchWeb,
|
|
+ buildfile.workerExtensionHost,
|
|
+ buildfile.keyboardMaps,
|
|
+ buildfile.entrypoint("vs/platform/files/node/watcher/unix/watcherApp", ["vs/css", "vs/nls"]),
|
|
+ buildfile.entrypoint("vs/platform/files/node/watcher/nsfw/watcherApp", ["vs/css", "vs/nls"]),
|
|
+ buildfile.entrypoint("vs/workbench/services/extensions/node/extensionHostProcess", ["vs/css", "vs/nls"]),
|
|
+]);
|
|
+
|
|
+const vscodeResources = [
|
|
+ "out-build/vs/server/fork.js",
|
|
+ "out-build/vs/server/node/uriTransformer.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/paths.js",
|
|
+ 'out-build/vs/**/*.{svg,png,html}',
|
|
+ "!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/octiconLabel/octicons/**",
|
|
+ "out-build/vs/base/browser/ui/codiconLabel/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/**"
|
|
+];
|
|
+
|
|
+const rootPath = __dirname;
|
|
+const nodeModules = ["electron", "original-fs"]
|
|
+ .concat(_.uniq(deps.getProductionDependencies(rootPath).map((d) => d.name)))
|
|
+ .concat(_.uniq(deps.getProductionDependencies(path.join(rootPath, "src/vs/server")).map((d) => d.name)))
|
|
+ .concat(Object.keys(process.binding("natives")).filter((n) => !/^_|\//.test(n)));
|
|
+
|
|
+gulp.task("optimize", gulp.series(
|
|
+ util.rimraf("out-vscode"),
|
|
+ common.optimizeTask({
|
|
+ src: "out-build",
|
|
+ entryPoints: vscodeEntryPoints,
|
|
+ resources: vscodeResources,
|
|
+ loaderConfig: common.loaderConfig(nodeModules),
|
|
+ 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/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json
|
|
index 8ac6b2806c..8562a284db 100644
|
|
--- a/extensions/vscode-api-tests/package.json
|
|
+++ b/extensions/vscode-api-tests/package.json
|
|
@@ -121,7 +121,7 @@
|
|
"@types/node": "^12.11.7",
|
|
"mocha-junit-reporter": "^1.17.0",
|
|
"mocha-multi-reporters": "^1.1.7",
|
|
- "typescript": "^1.6.2",
|
|
+ "typescript": "3.7.2",
|
|
"vscode": "1.1.5"
|
|
}
|
|
}
|
|
diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock
|
|
index 2d8b725ff2..a8d93a17ca 100644
|
|
--- a/extensions/vscode-api-tests/yarn.lock
|
|
+++ b/extensions/vscode-api-tests/yarn.lock
|
|
@@ -1855,10 +1855,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
|
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
|
|
|
-typescript@^1.6.2:
|
|
- version "1.8.10"
|
|
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e"
|
|
- integrity sha1-tHXW4N/wv1DyluXKbvn7tccyDx4=
|
|
+typescript@3.7.2:
|
|
+ version "3.7.2"
|
|
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
|
|
+ integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
|
|
|
|
unique-stream@^2.0.2:
|
|
version "2.2.1"
|
|
diff --git a/package.json b/package.json
|
|
index 6e9b9dc0a0..49b14e536a 100644
|
|
--- a/package.json
|
|
+++ b/package.json
|
|
@@ -33,6 +33,9 @@
|
|
"eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions"
|
|
},
|
|
"dependencies": {
|
|
+ "@coder/logger": "^1.1.12",
|
|
+ "@coder/node-browser": "^1.0.8",
|
|
+ "@coder/requirefs": "^1.1.5",
|
|
"applicationinsights": "1.0.8",
|
|
"chokidar": "3.2.3",
|
|
"graceful-fs": "4.2.3",
|
|
diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts
|
|
index a68e020f9f..c31e7befa3 100644
|
|
--- a/src/vs/base/common/network.ts
|
|
+++ b/src/vs/base/common/network.ts
|
|
@@ -88,16 +88,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 5a631e0b39..4114bd9287 100644
|
|
--- a/src/vs/base/common/platform.ts
|
|
+++ b/src/vs/base/common/platform.ts
|
|
@@ -59,6 +59,17 @@ if (typeof navigator === 'object' && !isElectronRenderer) {
|
|
_isWeb = true;
|
|
_locale = navigator.language;
|
|
_language = _locale;
|
|
+ // NOTE@coder: Make languages work.
|
|
+ const el = typeof document !== 'undefined' && document.getElementById('vscode-remote-nls-configuration');
|
|
+ const rawNlsConfig = el && el.getAttribute('data-settings');
|
|
+ if (rawNlsConfig) {
|
|
+ try {
|
|
+ const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig);
|
|
+ _locale = nlsConfig.locale;
|
|
+ _translationsConfigFile = nlsConfig._translationsConfigFile;
|
|
+ _language = nlsConfig.availableLanguages['*'] || LANGUAGE_DEFAULT;
|
|
+ } catch (error) { /* Oh well. */ }
|
|
+ }
|
|
} else if (typeof process === 'object') {
|
|
_isWindows = (process.platform === 'win32');
|
|
_isMacintosh = (process.platform === 'darwin');
|
|
diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts
|
|
index c52f7b3774..08a87fa970 100644
|
|
--- a/src/vs/base/common/processes.ts
|
|
+++ b/src/vs/base/common/processes.ts
|
|
@@ -110,7 +110,8 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve
|
|
/^ELECTRON_.+$/,
|
|
/^GOOGLE_API_KEY$/,
|
|
/^VSCODE_.+$/,
|
|
- /^SNAP(|_.*)$/
|
|
+ /^SNAP(|_.*)$/,
|
|
+ /^CODE_SERVER_.+$/,
|
|
];
|
|
const envKeys = Object.keys(env);
|
|
envKeys
|
|
diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js
|
|
index 2c64061da7..c0ef8faedd 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 45f6f17ce0..79fde0b92c 100644
|
|
--- a/src/vs/code/browser/workbench/workbench.ts
|
|
+++ b/src/vs/code/browser/workbench/workbench.ts
|
|
@@ -246,12 +246,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
|
|
+ ? encodeURIComponent(workspace.folderUri.path).replace(/%2F/g, "/")
|
|
+ : 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
|
|
+ ? encodeURIComponent(workspace.workspaceUri.path).replace(/%2F/g, "/")
|
|
+ : encodeURIComponent(workspace.workspaceUri.toString());
|
|
+ targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${target}`;
|
|
}
|
|
|
|
// Append payload if any
|
|
@@ -302,35 +308,6 @@ class WorkspaceProvider implements IWorkspaceProvider {
|
|
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:
|
|
- payload = JSON.parse(value);
|
|
- break;
|
|
- }
|
|
- });
|
|
-
|
|
// If no workspace is provided through the URL, check for config attribute from server
|
|
if (!foundWorkspace) {
|
|
if (config.folderUri) {
|
|
diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts
|
|
index abd1e33b18..bf75952ce1 100644
|
|
--- a/src/vs/platform/environment/common/environment.ts
|
|
+++ b/src/vs/platform/environment/common/environment.ts
|
|
@@ -37,6 +37,8 @@ export interface ParsedArgs {
|
|
logExtensionHostCommunication?: boolean;
|
|
'extensions-dir'?: string;
|
|
'builtin-extensions-dir'?: string;
|
|
+ 'extra-extensions-dir'?: string[];
|
|
+ 'extra-builtin-extensions-dir'?: string[];
|
|
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
|
|
extensionTestsPath?: string; // either a local path or a URI
|
|
'extension-development-confirm-save'?: boolean;
|
|
@@ -147,6 +149,8 @@ export interface IEnvironmentService extends IUserHomeProvider {
|
|
disableExtensions: boolean | string[];
|
|
builtinExtensionsPath: string;
|
|
extensionsPath?: string;
|
|
+ extraExtensionPaths: string[];
|
|
+ extraBuiltinExtensionPaths: string[];
|
|
extensionDevelopmentLocationURI?: URI[];
|
|
extensionTestsLocationURI?: URI;
|
|
logExtensionHostCommunication?: boolean;
|
|
diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts
|
|
index e68e0647c3..49a5aae2fa 100644
|
|
--- a/src/vs/platform/environment/node/argv.ts
|
|
+++ b/src/vs/platform/environment/node/argv.ts
|
|
@@ -55,6 +55,8 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
|
|
|
|
'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") },
|
|
'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.") },
|
|
@@ -310,4 +312,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 15b5c20cbb..a95f1cd7b5 100644
|
|
--- a/src/vs/platform/environment/node/environmentService.ts
|
|
+++ b/src/vs/platform/environment/node/environmentService.ts
|
|
@@ -197,6 +197,13 @@ export class EnvironmentService implements IEnvironmentService {
|
|
return path.join(this.userHome, product.dataFolderName, 'extensions');
|
|
}
|
|
|
|
+ @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/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
|
index 5b05650591..aa8712d8fb 100644
|
|
--- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
|
+++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts
|
|
@@ -743,11 +743,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
|
|
|
private scanSystemExtensions(): Promise<ILocalExtension[]> {
|
|
this.logService.trace('Started scanning system extensions');
|
|
- const systemExtensionsPromise = this.scanExtensions(this.systemExtensionsPath, ExtensionType.System)
|
|
- .then(result => {
|
|
- this.logService.trace('Scanned system extensions:', result.length);
|
|
- return result;
|
|
- });
|
|
+ const systemExtensionsPromise = Promise.all([
|
|
+ this.scanExtensions(this.systemExtensionsPath, ExtensionType.System),
|
|
+ ...this.environmentService.extraBuiltinExtensionPaths
|
|
+ .map((path) => this.scanExtensions(path, ExtensionType.System))
|
|
+ ]).then((results) => {
|
|
+ const result = results.reduce((flat, current) => flat.concat(current), []);
|
|
+ this.logService.trace('Scanned system extensions:', result.length);
|
|
+ return result;
|
|
+ });
|
|
if (this.environmentService.isBuilt) {
|
|
return systemExtensionsPromise;
|
|
}
|
|
@@ -769,9 +773,16 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
|
.then(([systemExtensions, devSystemExtensions]) => [...systemExtensions, ...devSystemExtensions]);
|
|
}
|
|
|
|
+ private scanAllUserExtensions(folderName: string, type: ExtensionType): Promise<ILocalExtension[]> {
|
|
+ return Promise.all([
|
|
+ this.scanExtensions(folderName, type),
|
|
+ ...this.environmentService.extraExtensionPaths.map((p) => this.scanExtensions(p, ExtensionType.User))
|
|
+ ]).then((results) => results.reduce((flat, current) => flat.concat(current), []));
|
|
+ }
|
|
+
|
|
private scanUserExtensions(excludeOutdated: boolean): Promise<ILocalExtension[]> {
|
|
this.logService.trace('Started scanning user extensions');
|
|
- return Promise.all([this.getUninstalledExtensions(), this.scanExtensions(this.extensionsPath, ExtensionType.User)])
|
|
+ return Promise.all([this.getUninstalledExtensions(), this.scanAllUserExtensions(this.extensionsPath, ExtensionType.User)])
|
|
.then(([uninstalled, extensions]) => {
|
|
extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]);
|
|
if (excludeOutdated) {
|
|
@@ -786,6 +797,12 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
|
private scanExtensions(root: string, type: ExtensionType): Promise<ILocalExtension[]> {
|
|
const limiter = new Limiter<any>(10);
|
|
return pfs.readdir(root)
|
|
+ .catch((error) => {
|
|
+ if (error.code !== 'ENOENT') {
|
|
+ throw error;
|
|
+ }
|
|
+ return <string[]>[];
|
|
+ })
|
|
.then(extensionsFolders => Promise.all<ILocalExtension>(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, root, type)))))
|
|
.then(extensions => extensions.filter(e => e && e.identifier));
|
|
}
|
|
@@ -824,7 +841,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
|
|
|
private async removeUninstalledExtensions(): Promise<void> {
|
|
const uninstalled = await this.getUninstalledExtensions();
|
|
- const extensions = await this.scanExtensions(this.extensionsPath, ExtensionType.User); // All user extensions
|
|
+ const extensions = await this.scanAllUserExtensions(this.extensionsPath, ExtensionType.User); // All user extensions
|
|
const installed: Set<string> = new Set<string>();
|
|
for (const e of extensions) {
|
|
if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) {
|
|
@@ -843,7 +860,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
|
}
|
|
|
|
private removeOutdatedExtensions(): Promise<void> {
|
|
- return this.scanExtensions(this.extensionsPath, ExtensionType.User) // All user extensions
|
|
+ return this.scanAllUserExtensions(this.extensionsPath, ExtensionType.User) // All user extensions
|
|
.then(extensions => {
|
|
const toRemove: ILocalExtension[] = [];
|
|
|
|
diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts
|
|
index da88376513..5bab4bd49c 100644
|
|
--- a/src/vs/platform/product/common/product.ts
|
|
+++ b/src/vs/platform/product/common/product.ts
|
|
@@ -27,6 +27,12 @@ if (isWeb) {
|
|
urlProtocol: 'code-oss'
|
|
});
|
|
}
|
|
+ // 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) {
|
|
+ assign(product, JSON.parse(rawProductConfiguration));
|
|
+ }
|
|
}
|
|
|
|
// Node: AMD loader
|
|
diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts
|
|
index d0f6e6b18a..1966fd297d 100644
|
|
--- a/src/vs/platform/remote/browser/browserSocketFactory.ts
|
|
+++ b/src/vs/platform/remote/browser/browserSocketFactory.ts
|
|
@@ -205,7 +205,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();
|
|
@@ -213,6 +214,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 eab8591492..26668701f7 100644
|
|
--- a/src/vs/platform/remote/common/remoteAgentConnection.ts
|
|
+++ b/src/vs/platform/remote/common/remoteAgentConnection.ts
|
|
@@ -88,7 +88,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/server/browser/client.ts b/src/vs/server/browser/client.ts
|
|
new file mode 100644
|
|
index 0000000000..4042e32f74
|
|
--- /dev/null
|
|
+++ b/src/vs/server/browser/client.ts
|
|
@@ -0,0 +1,263 @@
|
|
+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 { ILogService } from 'vs/platform/log/common/log';
|
|
+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';
|
|
+
|
|
+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 from the server.
|
|
+ */
|
|
+export const getOptions = <T extends Options>(): T => {
|
|
+ if (typeof document === "undefined") {
|
|
+ return {} as T;
|
|
+ }
|
|
+ const el = document.getElementById("coder-options");
|
|
+ try {
|
|
+ if (!el) {
|
|
+ throw new Error("no options element");
|
|
+ }
|
|
+ const value = el.getAttribute("data-settings");
|
|
+ if (!value) {
|
|
+ throw new Error("no options value");
|
|
+ }
|
|
+ const options = JSON.parse(value);
|
|
+ const parts = window.location.pathname.replace(/^\//g, "").split("/");
|
|
+ parts[parts.length - 1] = options.base;
|
|
+ const url = new URL(window.location.origin + "/" + parts.join("/"));
|
|
+ return {
|
|
+ ...options,
|
|
+ base: normalize(url.pathname, true),
|
|
+ };
|
|
+ } catch (error) {
|
|
+ console.warn(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();
|
|
+ }
|
|
+ }],
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ const applyUpdate = async (): Promise<void> => {
|
|
+ (services.get(ILogService) as ILogService).debug("Applying update...");
|
|
+
|
|
+ const response = await fetch(normalize(`${options.base}/update/apply`), {
|
|
+ headers: { "content-type": "application/json" },
|
|
+ });
|
|
+ if (response.status !== 200) {
|
|
+ throw new Error("Unexpected response");
|
|
+ }
|
|
+
|
|
+ const json = await response.json();
|
|
+ if (!json.isLatest) {
|
|
+ throw new Error("Update failed");
|
|
+ }
|
|
+
|
|
+ (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`);
|
|
+ };
|
|
+
|
|
+ const getUpdate = async (): Promise<void> => {
|
|
+ (services.get(ILogService) as ILogService).debug("Checking for update...");
|
|
+
|
|
+ const response = await fetch(normalize(`${options.base}/update`), {
|
|
+ headers: { "content-type": "application/json" },
|
|
+ });
|
|
+ if (response.status !== 200) {
|
|
+ throw new Error("unexpected response");
|
|
+ }
|
|
+
|
|
+ const json = await response.json();
|
|
+ if (json.isLatest) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ (services.get(INotificationService) as INotificationService).notify({
|
|
+ severity: Severity.Info,
|
|
+ message: `code-server has an update: ${json.version}`,
|
|
+ actions: {
|
|
+ primary: [{
|
|
+ id: 'update',
|
|
+ label: 'Apply Update',
|
|
+ tooltip: '',
|
|
+ class: undefined,
|
|
+ enabled: true,
|
|
+ checked: true,
|
|
+ dispose: () => undefined,
|
|
+ run: applyUpdate,
|
|
+ }],
|
|
+ }
|
|
+ });
|
|
+ };
|
|
+
|
|
+ const updateLoop = (): void => {
|
|
+ getUpdate().catch((error) => {
|
|
+ (services.get(ILogService) as ILogService).warn(error);
|
|
+ }).finally(() => {
|
|
+ setTimeout(updateLoop, 300000);
|
|
+ });
|
|
+ };
|
|
+
|
|
+ updateLoop();
|
|
+};
|
|
+
|
|
+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 0000000000..ed7c078077
|
|
--- /dev/null
|
|
+++ b/src/vs/server/browser/extHostNodeProxy.ts
|
|
@@ -0,0 +1,46 @@
|
|
+import { Emitter } from 'vs/base/common/event';
|
|
+import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
|
+import { ExtHostNodeProxyShape, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol';
|
|
+import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
|
+
|
|
+export class ExtHostNodeProxy implements ExtHostNodeProxyShape {
|
|
+ _serviceBrand: any;
|
|
+
|
|
+ private readonly _onMessage = new Emitter<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);
|
|
+ }
|
|
+}
|
|
+
|
|
+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 0000000000..0d2e93edae
|
|
--- /dev/null
|
|
+++ b/src/vs/server/browser/mainThreadNodeProxy.ts
|
|
@@ -0,0 +1,37 @@
|
|
+import { IDisposable } from 'vs/base/common/lifecycle';
|
|
+import { INodeProxyService } from 'vs/server/common/nodeProxy';
|
|
+import { ExtHostContext, IExtHostContext, MainContext, MainThreadNodeProxyShape } from 'vs/workbench/api/common/extHost.protocol';
|
|
+import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
|
+
|
|
+@extHostNamedCustomer(MainContext.MainThreadNodeProxy)
|
|
+export class MainThreadNodeProxy implements MainThreadNodeProxyShape {
|
|
+ private disposed = false;
|
|
+ private disposables = <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);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ 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 0000000000..a93381631a
|
|
--- /dev/null
|
|
+++ b/src/vs/server/browser/worker.ts
|
|
@@ -0,0 +1,57 @@
|
|
+import { Client } from '@coder/node-browser';
|
|
+import { fromTar } from '@coder/requirefs';
|
|
+import { URI } from 'vs/base/common/uri';
|
|
+import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
+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: IExtensionDescription,
|
|
+ activationTimesBuilder: ExtensionActivationTimesBuilder,
|
|
+ nodeProxy: IExtHostNodeProxy,
|
|
+ logService: ILogService,
|
|
+ vscode: any,
|
|
+): Promise<T> => {
|
|
+ const fetchUri = URI.from({
|
|
+ scheme: self.location.protocol.replace(':', ''),
|
|
+ authority: self.location.host,
|
|
+ path: self.location.pathname.replace(/\/static\/([^\/]+)\/.*$/, '/static/$1\/'),
|
|
+ query: `tar=${encodeURIComponent(module.extensionLocation.path)}`,
|
|
+ });
|
|
+ const response = await fetch(fetchUri.toString(true));
|
|
+ if (response.status !== 200) {
|
|
+ throw new Error(`Failed to download extension "${module.extensionLocation.path}"`);
|
|
+ }
|
|
+ const client = new Client(nodeProxy, { logger: logService });
|
|
+ const init = await client.handshake();
|
|
+ const buffer = new Uint8Array(await response.arrayBuffer());
|
|
+ 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 0000000000..14b9de879c
|
|
--- /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 0000000000..eb62b87798
|
|
--- /dev/null
|
|
+++ b/src/vs/server/common/telemetry.ts
|
|
@@ -0,0 +1,49 @@
|
|
+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 'setEnabled': return Promise.resolve(this.service.setEnabled(args[0]));
|
|
+ case 'getTelemetryInfo': return this.service.getTelemetryInfo();
|
|
+ }
|
|
+ throw new Error(`Invalid call ${command}`);
|
|
+ }
|
|
+}
|
|
+
|
|
+export class TelemetryChannelClient implements ITelemetryService {
|
|
+ _serviceBrand: any;
|
|
+
|
|
+ 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 setEnabled(value: boolean): void {
|
|
+ this.channel.call('setEnable', [value]);
|
|
+ }
|
|
+
|
|
+ public getTelemetryInfo(): Promise<ITelemetryInfo> {
|
|
+ return this.channel.call('getTelemetryInfo');
|
|
+ }
|
|
+
|
|
+ public get isOptedIn(): boolean {
|
|
+ return true;
|
|
+ }
|
|
+}
|
|
diff --git a/src/vs/server/entry.ts b/src/vs/server/entry.ts
|
|
new file mode 100644
|
|
index 0000000000..ab020fbb4e
|
|
--- /dev/null
|
|
+++ b/src/vs/server/entry.ts
|
|
@@ -0,0 +1,78 @@
|
|
+import { field } from '@coder/logger';
|
|
+import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
|
|
+import { CodeServerMessage, VscodeMessage } from 'vs/server/ipc';
|
|
+import { logger } from 'vs/server/node/logger';
|
|
+import { enableCustomMarketplace } from 'vs/server/node/marketplace';
|
|
+import { Vscode } from 'vs/server/node/server';
|
|
+
|
|
+setUnexpectedErrorHandler((error) => logger.warn(error instanceof Error ? error.message : error));
|
|
+enableCustomMarketplace();
|
|
+
|
|
+/**
|
|
+ * Ensure we control when the process exits.
|
|
+ */
|
|
+const exit = process.exit;
|
|
+process.exit = function(code?: number) {
|
|
+ logger.warn(`process.exit() was prevented: ${code || 'unknown code'}.`);
|
|
+} as (code?: number) => never;
|
|
+
|
|
+// Kill VS Code if the parent process dies.
|
|
+if (typeof process.env.CODE_SERVER_PARENT_PID !== 'undefined') {
|
|
+ const parentPid = parseInt(process.env.CODE_SERVER_PARENT_PID, 10);
|
|
+ setInterval(() => {
|
|
+ try {
|
|
+ process.kill(parentPid, 0); // Throws an exception if the process doesn't exist anymore.
|
|
+ } catch (e) {
|
|
+ exit();
|
|
+ }
|
|
+ }, 5000);
|
|
+} else {
|
|
+ logger.error('no parent process');
|
|
+ exit(1);
|
|
+}
|
|
+
|
|
+const vscode = new Vscode();
|
|
+const send = (message: VscodeMessage): void => {
|
|
+ if (!process.send) {
|
|
+ throw new Error('not spawned with IPC');
|
|
+ }
|
|
+ process.send(message);
|
|
+};
|
|
+
|
|
+// Wait for the init message then start up VS Code. Subsequent messages will
|
|
+// return new workbench options without starting a new instance.
|
|
+process.on('message', async (message: CodeServerMessage, socket) => {
|
|
+ logger.debug('got message from code-server', field('message', message));
|
|
+ switch (message.type) {
|
|
+ case 'init':
|
|
+ try {
|
|
+ const options = await vscode.initialize(message.options);
|
|
+ send({ type: 'options', id: message.id, options });
|
|
+ } catch (error) {
|
|
+ logger.error(error.message);
|
|
+ logger.error(error.stack);
|
|
+ exit(1);
|
|
+ }
|
|
+ break;
|
|
+ case 'cli':
|
|
+ try {
|
|
+ await vscode.cli(message.args);
|
|
+ exit(0);
|
|
+ } catch (error) {
|
|
+ logger.error(error.message);
|
|
+ logger.error(error.stack);
|
|
+ exit(1);
|
|
+ }
|
|
+ break;
|
|
+ case 'socket':
|
|
+ vscode.handleWebSocket(socket, message.query);
|
|
+ break;
|
|
+ }
|
|
+});
|
|
+if (!process.send) {
|
|
+ logger.error('not spawned with IPC');
|
|
+ exit(1);
|
|
+} else {
|
|
+ // This lets the parent know the child is ready to receive messages.
|
|
+ send({ type: 'ready' });
|
|
+}
|
|
diff --git a/src/vs/server/fork.js b/src/vs/server/fork.js
|
|
new file mode 100644
|
|
index 0000000000..56331ff1fc
|
|
--- /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 0000000000..cb4d3a6afe
|
|
--- /dev/null
|
|
+++ b/src/vs/server/ipc.d.ts
|
|
@@ -0,0 +1,113 @@
|
|
+/**
|
|
+ * External interfaces for integration into code-server over IPC. No vs imports
|
|
+ * should be made in this file.
|
|
+ */
|
|
+export interface Options {
|
|
+ base: string
|
|
+ commit: string
|
|
+ disableTelemetry: boolean
|
|
+}
|
|
+
|
|
+export interface InitMessage {
|
|
+ type: 'init';
|
|
+ id: string;
|
|
+ options: VscodeOptions;
|
|
+}
|
|
+
|
|
+export type Query = { [key: string]: string | string[] | undefined };
|
|
+
|
|
+export interface SocketMessage {
|
|
+ type: 'socket';
|
|
+ query: Query;
|
|
+}
|
|
+
|
|
+export interface CliMessage {
|
|
+ type: 'cli';
|
|
+ args: Args;
|
|
+}
|
|
+
|
|
+export 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;
|
|
+
|
|
+ '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 remoteUserDataUri: UriComponents;
|
|
+ readonly productConfiguration: {
|
|
+ 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 0000000000..9c240b992d
|
|
--- /dev/null
|
|
+++ b/src/vs/server/node/channel.ts
|
|
@@ -0,0 +1,343 @@
|
|
+import { Server } from '@coder/node-browser';
|
|
+import * as path from 'path';
|
|
+import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
|
|
+import { Emitter, Event } from 'vs/base/common/event';
|
|
+import { IDisposable } from 'vs/base/common/lifecycle';
|
|
+import { OS } from 'vs/base/common/platform';
|
|
+import { ReadableStreamEventPayload } from 'vs/base/common/stream';
|
|
+import { URI, UriComponents } from 'vs/base/common/uri';
|
|
+import { transformOutgoingURIs } from 'vs/base/common/uriIpc';
|
|
+import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
|
+import { IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics';
|
|
+import { IEnvironmentService } 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 { createReadStream } from 'vs/platform/files/common/io';
|
|
+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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|
+import { INodeProxyService } from 'vs/server/common/nodeProxy';
|
|
+import { getTranslations } from 'vs/server/node/nls';
|
|
+import { getUriTransformer } from 'vs/server/node/util';
|
|
+import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol';
|
|
+import { ExtensionScanner, ExtensionScannerInput } from 'vs/workbench/services/extensions/node/extensionPoints';
|
|
+
|
|
+/**
|
|
+ * Extend the file provider to allow unwatching.
|
|
+ */
|
|
+class Watcher extends DiskFileSystemProvider {
|
|
+ public readonly watches = new Map<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: IEnvironmentService,
|
|
+ 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>> {
|
|
+ let fileStream: VSBufferReadableStream | undefined;
|
|
+ const emitter = new Emitter<ReadableStreamEventPayload<VSBuffer>>({
|
|
+ onFirstListenerAdd: () => {
|
|
+ if (!fileStream) {
|
|
+ fileStream = createReadStream(this.provider, this.transform(resource), {
|
|
+ ...opts,
|
|
+ bufferSize: 64 * 1024, // From DiskFileSystemProvider
|
|
+ });
|
|
+ fileStream.on('data', (data) => emitter.fire(data));
|
|
+ fileStream.on('error', (error) => emitter.fire(error));
|
|
+ fileStream.on('end', () => emitter.fire('end'));
|
|
+ }
|
|
+ },
|
|
+ onLastListenerRemove: () => fileStream && fileStream.destroy(),
|
|
+ });
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+}
|
|
+
|
|
+export class ExtensionEnvironmentChannel implements IServerChannel {
|
|
+ public constructor(
|
|
+ private readonly environment: IEnvironmentService,
|
|
+ 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(args.language),
|
|
+ getUriTransformer(context.remoteAuthority),
|
|
+ );
|
|
+ case 'getDiagnosticInfo': return this.getDiagnosticInfo();
|
|
+ case 'disableTelemetry': return this.disableTelemetry();
|
|
+ }
|
|
+ throw new Error(`Invalid call '${command}'`);
|
|
+ }
|
|
+
|
|
+ private async getEnvironmentData(locale: string): Promise<IRemoteAgentEnvironment> {
|
|
+ return {
|
|
+ pid: process.pid,
|
|
+ connectionToken: this.connectionToken,
|
|
+ appRoot: URI.file(this.environment.appRoot),
|
|
+ appSettingsHome: this.environment.appSettingsHome,
|
|
+ settingsPath: this.environment.machineSettingsHome,
|
|
+ logsPath: URI.file(this.environment.logsPath),
|
|
+ extensionsPath: URI.file(this.environment.extensionsPath!),
|
|
+ extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, 'extension-host')),
|
|
+ globalStorageHome: URI.file(this.environment.globalStorageHome),
|
|
+ userHome: URI.file(this.environment.userHome),
|
|
+ extensions: await this.scanExtensions(locale),
|
|
+ os: OS,
|
|
+ };
|
|
+ }
|
|
+
|
|
+ private async scanExtensions(locale: string): Promise<IExtensionDescription[]> {
|
|
+ const translations = await getTranslations(locale, 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,
|
|
+ locale,
|
|
+ !!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);
|
|
+ });
|
|
+ });
|
|
+ });
|
|
+ return Array.from(uniqueExtensions.values());
|
|
+ });
|
|
+ }
|
|
+
|
|
+ private getDiagnosticInfo(): Promise<IDiagnosticInfo> {
|
|
+ throw new Error('not implemented');
|
|
+ }
|
|
+
|
|
+ private async disableTelemetry(): Promise<void> {
|
|
+ this.telemetry.setEnabled(false);
|
|
+ }
|
|
+}
|
|
+
|
|
+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);
|
|
+ }
|
|
+}
|
|
diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts
|
|
new file mode 100644
|
|
index 0000000000..9b8969690c
|
|
--- /dev/null
|
|
+++ b/src/vs/server/node/connection.ts
|
|
@@ -0,0 +1,158 @@
|
|
+import * as cp from 'child_process';
|
|
+import { getPathFromAmdModule } from 'vs/base/common/amd';
|
|
+import { VSBuffer } from 'vs/base/common/buffer';
|
|
+import { Emitter } from 'vs/base/common/event';
|
|
+import { ISocket } from 'vs/base/parts/ipc/common/ipc.net';
|
|
+import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
|
+import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
|
+import { ILogService } from 'vs/platform/log/common/log';
|
|
+import { getNlsConfiguration } from 'vs/server/node/nls';
|
|
+import { Protocol } from 'vs/server/node/protocol';
|
|
+import { uriTransformerPath } from 'vs/server/node/util';
|
|
+import { IExtHostReadyMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
|
+
|
|
+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.getSocket().end();
|
|
+ }
|
|
+
|
|
+ protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
|
|
+ this.protocol.beginAcceptReconnection(socket, buffer);
|
|
+ this.protocol.endAcceptReconnection();
|
|
+ }
|
|
+}
|
|
+
|
|
+export class ExtensionHostConnection extends Connection {
|
|
+ private process?: cp.ChildProcess;
|
|
+
|
|
+ public constructor(
|
|
+ locale:string, protocol: Protocol, buffer: VSBuffer, token: string,
|
|
+ private readonly log: ILogService,
|
|
+ private readonly environment: IEnvironmentService,
|
|
+ ) {
|
|
+ super(protocol, token);
|
|
+ this.protocol.dispose();
|
|
+ this.spawn(locale, buffer).then((p) => this.process = p);
|
|
+ this.protocol.getUnderlyingSocket().pause();
|
|
+ }
|
|
+
|
|
+ protected doDispose(): void {
|
|
+ if (this.process) {
|
|
+ this.process.kill();
|
|
+ }
|
|
+ this.protocol.getSocket().end();
|
|
+ }
|
|
+
|
|
+ protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
|
|
+ // This is just to set the new socket.
|
|
+ this.protocol.beginAcceptReconnection(socket, null);
|
|
+ this.protocol.dispose();
|
|
+ this.sendInitMessage(buffer);
|
|
+ }
|
|
+
|
|
+ private sendInitMessage(buffer: VSBuffer): void {
|
|
+ const socket = this.protocol.getUnderlyingSocket();
|
|
+ socket.pause();
|
|
+ this.process!.send({ // Process must be set at this point.
|
|
+ type: 'VSCODE_EXTHOST_IPC_SOCKET',
|
|
+ initialDataChunk: (buffer.buffer as Buffer).toString('base64'),
|
|
+ skipWebSocketFrames: this.protocol.getSocket() instanceof NodeSocket,
|
|
+ }, socket);
|
|
+ }
|
|
+
|
|
+ private async spawn(locale: string, buffer: VSBuffer): Promise<cp.ChildProcess> {
|
|
+ const config = await getNlsConfiguration(locale, this.environment.userDataPath);
|
|
+ const proc = cp.fork(
|
|
+ getPathFromAmdModule(require, 'bootstrap-fork'),
|
|
+ [ '--type=extensionHost', `--uriTransformerPath=${uriTransformerPath}` ],
|
|
+ {
|
|
+ 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: this.environment.verbose ? 'trace' : this.environment.log,
|
|
+ VSCODE_NLS_CONFIG: JSON.stringify(config),
|
|
+ },
|
|
+ silent: true,
|
|
+ },
|
|
+ );
|
|
+
|
|
+ proc.on('error', () => this.dispose());
|
|
+ proc.on('exit', () => this.dispose());
|
|
+ if (proc.stdout && proc.stderr) {
|
|
+ proc.stdout.setEncoding('utf8').on('data', (d) => this.log.info('Extension host stdout', d));
|
|
+ proc.stderr.setEncoding('utf8').on('data', (d) => this.log.error('Extension host stderr', d));
|
|
+ }
|
|
+ proc.on('message', (event) => {
|
|
+ if (event && event.type === '__$console') {
|
|
+ const severity = (<any>this.log)[event.severity] ? event.severity : 'info';
|
|
+ (<any>this.log)[severity]('Extension host', event.arguments);
|
|
+ }
|
|
+ if (event && event.type === 'VSCODE_EXTHOST_DISCONNECTED') {
|
|
+ this.setOffline();
|
|
+ }
|
|
+ });
|
|
+
|
|
+ const listen = (message: IExtHostReadyMessage) => {
|
|
+ if (message.type === 'VSCODE_EXTHOST_IPC_READY') {
|
|
+ proc.removeListener('message', listen);
|
|
+ this.sendInitMessage(buffer);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ return proc.on('message', listen);
|
|
+ }
|
|
+}
|
|
diff --git a/src/vs/server/node/insights.ts b/src/vs/server/node/insights.ts
|
|
new file mode 100644
|
|
index 0000000000..a0ece345f2
|
|
--- /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 0000000000..5e560eb46e
|
|
--- /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 0000000000..2a39c524aa
|
|
--- /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 0000000000..8956fc40d4
|
|
--- /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 0000000000..3d428a57d3
|
|
--- /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 0000000000..3c74512192
|
|
--- /dev/null
|
|
+++ b/src/vs/server/node/protocol.ts
|
|
@@ -0,0 +1,73 @@
|
|
+import * as net from 'net';
|
|
+import { VSBuffer } from 'vs/base/common/buffer';
|
|
+import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
|
|
+import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
|
+import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from 'vs/platform/remote/common/remoteAgentConnection';
|
|
+
|
|
+export interface SocketOptions {
|
|
+ readonly reconnectionToken: string;
|
|
+ readonly reconnection: boolean;
|
|
+ readonly skipWebSocketFrames: boolean;
|
|
+}
|
|
+
|
|
+export class Protocol extends PersistentProtocol {
|
|
+ public constructor(socket: net.Socket, public readonly options: SocketOptions) {
|
|
+ super(
|
|
+ options.skipWebSocketFrames
|
|
+ ? new NodeSocket(socket)
|
|
+ : new WebSocketNodeSocket(new NodeSocket(socket)),
|
|
+ );
|
|
+ }
|
|
+
|
|
+ public getUnderlyingSocket(): net.Socket {
|
|
+ const socket = this.getSocket();
|
|
+ return socket instanceof NodeSocket
|
|
+ ? socket.socket
|
|
+ : (socket as WebSocketNodeSocket).socket.socket;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Perform a handshake to get a connection request.
|
|
+ */
|
|
+ public handshake(): Promise<ConnectionTypeRequest> {
|
|
+ return new Promise((resolve, reject) => {
|
|
+ const handler = this.onControlMessage((rawMessage) => {
|
|
+ try {
|
|
+ const message = JSON.parse(rawMessage.toString());
|
|
+ switch (message.type) {
|
|
+ case 'auth': return this.authenticate(message);
|
|
+ case 'connectionType':
|
|
+ handler.dispose();
|
|
+ return resolve(message);
|
|
+ default: throw new Error('Unrecognized message type');
|
|
+ }
|
|
+ } catch (error) {
|
|
+ handler.dispose();
|
|
+ reject(error);
|
|
+ }
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * TODO: This ignores the authentication process entirely for now.
|
|
+ */
|
|
+ private authenticate(_message: AuthRequest): void {
|
|
+ this.sendMessage({ type: 'sign', data: '' });
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * TODO: implement.
|
|
+ */
|
|
+ public tunnel(): void {
|
|
+ throw new Error('Tunnel is not implemented yet');
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Send a handshake message. In the case of the extension host, it just sends
|
|
+ * back a debug port.
|
|
+ */
|
|
+ public sendMessage(message: HandshakeMessage | { debugPort?: number } ): void {
|
|
+ this.sendControl(VSBuffer.fromString(JSON.stringify(message)));
|
|
+ }
|
|
+}
|
|
diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts
|
|
new file mode 100644
|
|
index 0000000000..d6dcfe1fe7
|
|
--- /dev/null
|
|
+++ b/src/vs/server/node/server.ts
|
|
@@ -0,0 +1,257 @@
|
|
+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, IPCServer, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
|
+import { createChannelReceiver } from 'vs/base/parts/ipc/node/ipc';
|
|
+import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner';
|
|
+import { main } from "vs/code/node/cliProcessMain";
|
|
+import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
+import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
|
|
+import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
|
|
+import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
|
|
+import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
|
+import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
|
|
+import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
|
+import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
|
|
+import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
|
+import { IFileService } from 'vs/platform/files/common/files';
|
|
+import { FileService } from 'vs/platform/files/common/fileService';
|
|
+import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
|
+import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
|
+import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
|
|
+import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
|
+import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
|
|
+import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
|
|
+import { getLogLevel, ILogService } from 'vs/platform/log/common/log';
|
|
+import { LoggerChannel } from 'vs/platform/log/common/logIpc';
|
|
+import { SpdLogService } from 'vs/platform/log/node/spdlogService';
|
|
+import product from 'vs/platform/product/common/product';
|
|
+import { IProductService } from 'vs/platform/product/common/productService';
|
|
+import { ConnectionType, ConnectionTypeRequest } from 'vs/platform/remote/common/remoteAgentConnection';
|
|
+import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
|
+import { IRequestService } from 'vs/platform/request/common/request';
|
|
+import { RequestChannel } from 'vs/platform/request/common/requestIpc';
|
|
+import { RequestService } from 'vs/platform/request/node/requestService';
|
|
+import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry';
|
|
+import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|
+import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
|
|
+import { combinedAppender, LogAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
|
+import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
|
|
+import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
|
|
+import { INodeProxyService, NodeProxyChannel } from 'vs/server/common/nodeProxy';
|
|
+import { TelemetryChannel } from 'vs/server/common/telemetry';
|
|
+import { Query, VscodeOptions, WorkbenchOptions } from 'vs/server/ipc';
|
|
+import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from 'vs/server/node/channel';
|
|
+import { Connection, ExtensionHostConnection, ManagementConnection } from 'vs/server/node/connection';
|
|
+import { TelemetryClient } from 'vs/server/node/insights';
|
|
+import { logger } from 'vs/server/node/logger';
|
|
+import { getLocaleFromConfig, getNlsConfiguration } from 'vs/server/node/nls';
|
|
+import { Protocol } from 'vs/server/node/protocol';
|
|
+import { getUriTransformer } from 'vs/server/node/util';
|
|
+import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/workbench/services/remote/common/remoteAgentFileSystemChannel";
|
|
+import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService';
|
|
+
|
|
+export class Vscode {
|
|
+ public readonly _onDidClientConnect = new Emitter<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: ParsedArgs): 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 IEnvironmentService;
|
|
+ const startPath = options.startPath;
|
|
+ return {
|
|
+ workbenchWebConfiguration: {
|
|
+ workspaceUri: startPath && startPath.workspace ? URI.parse(startPath.url) : undefined,
|
|
+ folderUri: startPath && !startPath.workspace ? URI.parse(startPath.url) : undefined,
|
|
+ remoteAuthority: options.remoteAuthority,
|
|
+ logLevel: getLogLevel(environment),
|
|
+ },
|
|
+ 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'
|
|
+ );
|
|
+ }
|
|
+
|
|
+ protocol.sendMessage(await ok());
|
|
+
|
|
+ let connection: Connection;
|
|
+ if (message.desiredConnectionType === ConnectionType.Management) {
|
|
+ connection = new ManagementConnection(protocol, token);
|
|
+ this._onDidClientConnect.fire({
|
|
+ protocol, onDidClientDisconnect: connection.onClose,
|
|
+ });
|
|
+ // TODO: Need a way to match clients with a connection. For now
|
|
+ // dispose everything which only works because no extensions currently
|
|
+ // utilize long-running proxies.
|
|
+ (this.services.get(INodeProxyService) as NodeProxyService)._onUp.fire();
|
|
+ connection.onClose(() => (this.services.get(INodeProxyService) as NodeProxyService)._onDown.fire());
|
|
+ } else {
|
|
+ const buffer = protocol.readEntireBuffer();
|
|
+ connection = new ExtensionHostConnection(
|
|
+ message.args ? message.args.language : 'en',
|
|
+ protocol, buffer, token,
|
|
+ this.services.get(ILogService) as ILogService,
|
|
+ this.services.get(IEnvironmentService) as IEnvironmentService,
|
|
+ );
|
|
+ }
|
|
+ connections.set(token, connection);
|
|
+ connection.onClose(() => connections.delete(token));
|
|
+ this.disposeOldOfflineConnections(connections);
|
|
+ break;
|
|
+ case ConnectionType.Tunnel: return protocol.tunnel();
|
|
+ default: throw new Error('Unrecognized connection type');
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private disposeOldOfflineConnections(connections: Map<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) {
|
|
+ offline[i].dispose();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private async initializeServices(args: ParsedArgs): Promise<void> {
|
|
+ const environmentService = new EnvironmentService(args, process.execPath);
|
|
+ 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.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(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.machineSettingsResource, fileService]));
|
|
+ this.services.set(IRequestService, new SyncDescriptor(RequestService));
|
|
+ this.services.set(IFileService, fileService);
|
|
+ this.services.set(IProductService, { _serviceBrand: undefined, ...product });
|
|
+ this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
|
|
+ this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
|
|
+
|
|
+ if (!environmentService.args['disable-telemetry']) {
|
|
+ this.services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [{
|
|
+ appender: combinedAppender(
|
|
+ new AppInsightsAppender('code-server', null, () => new TelemetryClient() as any, logService),
|
|
+ new LogAppender(logService),
|
|
+ ),
|
|
+ commonProperties: resolveCommonProperties(
|
|
+ product.commit, product.version, await getMachineId(),
|
|
+ [], environmentService.installSourcePath, 'code-server',
|
|
+ ),
|
|
+ piiPaths,
|
|
+ } as ITelemetryServiceConfig]));
|
|
+ } else {
|
|
+ this.services.set(ITelemetryService, NullTelemetryService);
|
|
+ }
|
|
+
|
|
+ await new Promise((resolve) => {
|
|
+ const instantiationService = new InstantiationService(this.services);
|
|
+ this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService));
|
|
+ this.services.set(INodeProxyService, instantiationService.createInstance(NodeProxyService));
|
|
+
|
|
+ instantiationService.invokeFunction(() => {
|
|
+ instantiationService.createInstance(LogsDataCleaner);
|
|
+ const telemetryService = this.services.get(ITelemetryService) as ITelemetryService;
|
|
+ this.ipc.registerChannel('extensions', new ExtensionManagementChannel(
|
|
+ this.services.get(IExtensionManagementService) as IExtensionManagementService,
|
|
+ (context) => getUriTransformer(context.remoteAuthority),
|
|
+ ));
|
|
+ this.ipc.registerChannel('remoteextensionsenvironment', new ExtensionEnvironmentChannel(
|
|
+ environmentService, logService, telemetryService, '',
|
|
+ ));
|
|
+ this.ipc.registerChannel('request', new RequestChannel(this.services.get(IRequestService) as IRequestService));
|
|
+ this.ipc.registerChannel('telemetry', new TelemetryChannel(telemetryService));
|
|
+ this.ipc.registerChannel('nodeProxy', new NodeProxyChannel(this.services.get(INodeProxyService) as INodeProxyService));
|
|
+ this.ipc.registerChannel('localizations', <IServerChannel<any>>createChannelReceiver(this.services.get(ILocalizationsService) as ILocalizationsService));
|
|
+ this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService));
|
|
+ resolve(new ErrorTelemetry(telemetryService));
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * TODO: implement.
|
|
+ */
|
|
+ private async getDebugPort(): Promise<number | undefined> {
|
|
+ return undefined;
|
|
+ }
|
|
+}
|
|
diff --git a/src/vs/server/node/uriTransformer.js b/src/vs/server/node/uriTransformer.js
|
|
new file mode 100644
|
|
index 0000000000..fc69441cf0
|
|
--- /dev/null
|
|
+++ b/src/vs/server/node/uriTransformer.js
|
|
@@ -0,0 +1,24 @@
|
|
+// This file is included via a regular Node require. I'm not sure how (or if)
|
|
+// we can write this in Typescript and have it compile to non-AMD syntax.
|
|
+module.exports = (remoteAuthority) => {
|
|
+ return {
|
|
+ transformIncoming: (uri) => {
|
|
+ switch (uri.scheme) {
|
|
+ case "vscode-remote": return { scheme: "file", path: uri.path };
|
|
+ default: return uri;
|
|
+ }
|
|
+ },
|
|
+ transformOutgoing: (uri) => {
|
|
+ switch (uri.scheme) {
|
|
+ case "file": return { scheme: "vscode-remote", authority: remoteAuthority, path: uri.path };
|
|
+ default: return uri;
|
|
+ }
|
|
+ },
|
|
+ transformOutgoingScheme: (scheme) => {
|
|
+ switch (scheme) {
|
|
+ case "file": return "vscode-remote";
|
|
+ default: return scheme;
|
|
+ }
|
|
+ },
|
|
+ };
|
|
+};
|
|
diff --git a/src/vs/server/node/util.ts b/src/vs/server/node/util.ts
|
|
new file mode 100644
|
|
index 0000000000..06b080044c
|
|
--- /dev/null
|
|
+++ b/src/vs/server/node/util.ts
|
|
@@ -0,0 +1,9 @@
|
|
+import { getPathFromAmdModule } from 'vs/base/common/amd';
|
|
+import { URITransformer, IRawURITransformer } from 'vs/base/common/uriIpc';
|
|
+
|
|
+export const uriTransformerPath = getPathFromAmdModule(require, 'vs/server/node/uriTransformer');
|
|
+export const getUriTransformer = (remoteAuthority: string): URITransformer => {
|
|
+ const rawURITransformerFactory = <any>require.__$__nodeRequire(uriTransformerPath);
|
|
+ const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
|
|
+ return new URITransformer(rawURITransformer);
|
|
+};
|
|
diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
|
index e69aa80159..71a899d37b 100644
|
|
--- a/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
|
+++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts
|
|
@@ -58,6 +58,7 @@ import './mainThreadWorkspace';
|
|
import './mainThreadComments';
|
|
import './mainThreadTask';
|
|
import './mainThreadLabelService';
|
|
+import 'vs/server/browser/mainThreadNodeProxy';
|
|
import './mainThreadTunnelService';
|
|
import './mainThreadAuthentication';
|
|
import './mainThreadTimeline';
|
|
diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts
|
|
index cb57cc8227..9da59c028e 100644
|
|
--- a/src/vs/workbench/api/common/extHost.api.impl.ts
|
|
+++ b/src/vs/workbench/api/common/extHost.api.impl.ts
|
|
@@ -67,6 +67,7 @@ import { ILogService } from 'vs/platform/log/common/log';
|
|
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
|
|
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
|
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
|
|
+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';
|
|
@@ -91,6 +92,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|
const rpcProtocol = accessor.get(IExtHostRpcService);
|
|
const extHostStorage = accessor.get(IExtHostStorage);
|
|
const extHostLogService = accessor.get(ILogService);
|
|
+ const extHostNodeProxy = accessor.get(IExtHostNodeProxy);
|
|
const extHostTunnelService = accessor.get(IExtHostTunnelService);
|
|
const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService);
|
|
|
|
@@ -100,6 +102,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);
|
|
|
|
// automatically create and register addressable instances
|
|
diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts
|
|
index 10f51d2354..8dd34dffa9 100644
|
|
--- a/src/vs/workbench/api/common/extHost.protocol.ts
|
|
+++ b/src/vs/workbench/api/common/extHost.protocol.ts
|
|
@@ -671,6 +671,16 @@ export interface MainThreadLabelServiceShape extends IDisposable {
|
|
$unregisterResourceLabelFormatter(handle: number): void;
|
|
}
|
|
|
|
+export interface MainThreadNodeProxyShape extends IDisposable {
|
|
+ $send(message: string): void;
|
|
+}
|
|
+export interface ExtHostNodeProxyShape {
|
|
+ $onMessage(message: string): void;
|
|
+ $onClose(): void;
|
|
+ $onDown(): void;
|
|
+ $onUp(): void;
|
|
+}
|
|
+
|
|
export interface MainThreadSearchShape extends IDisposable {
|
|
$registerFileSearchProvider(handle: number, scheme: string): void;
|
|
$registerTextSearchProvider(handle: number, scheme: string): void;
|
|
@@ -1513,6 +1523,7 @@ export const MainContext = {
|
|
MainThreadTask: createMainId<MainThreadTaskShape>('MainThreadTask'),
|
|
MainThreadWindow: createMainId<MainThreadWindowShape>('MainThreadWindow'),
|
|
MainThreadLabelService: createMainId<MainThreadLabelServiceShape>('MainThreadLabelService'),
|
|
+ MainThreadNodeProxy: createMainId<MainThreadNodeProxyShape>('MainThreadNodeProxy'),
|
|
MainThreadTheming: createMainId<MainThreadThemingShape>('MainThreadTheming'),
|
|
MainThreadTunnelService: createMainId<MainThreadTunnelServiceShape>('MainThreadTunnelService'),
|
|
MainThreadTimeline: createMainId<MainThreadTimelineShape>('MainThreadTimeline')
|
|
@@ -1550,6 +1561,7 @@ export const ExtHostContext = {
|
|
ExtHostUrls: createExtId<ExtHostUrlsShape>('ExtHostUrls'),
|
|
ExtHostOutputService: createMainId<ExtHostOutputServiceShape>('ExtHostOutputService'),
|
|
ExtHostLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
|
|
+ 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 197aa88c85..1c337cdc39 100644
|
|
--- a/src/vs/workbench/api/common/extHostExtensionService.ts
|
|
+++ b/src/vs/workbench/api/common/extHostExtensionService.ts
|
|
@@ -32,6 +32,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData
|
|
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
|
|
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
|
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
|
+import { IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy';
|
|
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
|
|
|
interface ITestRunner {
|
|
@@ -77,6 +78,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio
|
|
protected readonly _extHostWorkspace: ExtHostWorkspace;
|
|
protected readonly _extHostConfiguration: ExtHostConfiguration;
|
|
protected readonly _logService: ILogService;
|
|
+ protected readonly _nodeProxy: IExtHostNodeProxy;
|
|
protected readonly _extHostTunnelService: IExtHostTunnelService;
|
|
|
|
protected readonly _mainThreadWorkspaceProxy: MainThreadWorkspaceShape;
|
|
@@ -107,7 +109,8 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio
|
|
@ILogService logService: ILogService,
|
|
@IExtHostInitDataService initData: IExtHostInitDataService,
|
|
@IExtensionStoragePaths storagePath: IExtensionStoragePaths,
|
|
- @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService
|
|
+ @IExtHostNodeProxy nodeProxy: IExtHostNodeProxy,
|
|
+ @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService,
|
|
) {
|
|
this._hostUtils = hostUtils;
|
|
this._extHostContext = extHostContext;
|
|
@@ -116,6 +119,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio
|
|
this._extHostWorkspace = extHostWorkspace;
|
|
this._extHostConfiguration = extHostConfiguration;
|
|
this._logService = logService;
|
|
+ this._nodeProxy = nodeProxy;
|
|
this._extHostTunnelService = extHostTunnelService;
|
|
this._disposables = new DisposableStore();
|
|
|
|
@@ -341,14 +345,14 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio
|
|
|
|
const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup);
|
|
return Promise.all([
|
|
- this._loadCommonJSModule<IExtensionModule>(joinPath(extensionDescription.extensionLocation, extensionDescription.main), activationTimesBuilder),
|
|
+ this._loadCommonJSModule<IExtensionModule>(extensionDescription, activationTimesBuilder),
|
|
this._loadExtensionContext(extensionDescription)
|
|
]).then(values => {
|
|
return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder);
|
|
});
|
|
}
|
|
|
|
- protected abstract _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T>;
|
|
+ protected abstract _loadCommonJSModule<T>(module: URI | IExtensionDescription, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T>;
|
|
|
|
private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise<vscode.ExtensionContext> {
|
|
|
|
diff --git a/src/vs/workbench/api/node/extHost.services.ts b/src/vs/workbench/api/node/extHost.services.ts
|
|
index 72ad75d63e..07b8a3f20c 100644
|
|
--- a/src/vs/workbench/api/node/extHost.services.ts
|
|
+++ b/src/vs/workbench/api/node/extHost.services.ts
|
|
@@ -24,6 +24,8 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa
|
|
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
|
|
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
|
|
import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
|
|
+import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
|
+import { IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
|
|
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
|
@@ -47,3 +49,19 @@ registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);
|
|
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
|
|
registerSingleton(IExtHostStorage, ExtHostStorage);
|
|
registerSingleton(IExtHostTunnelService, ExtHostTunnelService);
|
|
+
|
|
+function NotImplementedProxy<T>(name: ServiceIdentifier<T>): { new(): T } {
|
|
+ return <any>class {
|
|
+ constructor() {
|
|
+ return new Proxy({}, {
|
|
+ get(target: any, prop: string | number) {
|
|
+ if (target[prop]) {
|
|
+ return target[prop];
|
|
+ }
|
|
+ throw new Error(`Not Implemented: ${name}->${String(prop)}`);
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+ };
|
|
+}
|
|
+registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(IExtHostNodeProxy) {});
|
|
diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts
|
|
index 79189ba670..216f29b587 100644
|
|
--- a/src/vs/workbench/api/node/extHostExtensionService.ts
|
|
+++ b/src/vs/workbench/api/node/extHostExtensionService.ts
|
|
@@ -13,6 +13,8 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer
|
|
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { Schemas } from 'vs/base/common/network';
|
|
+import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
+import { joinPath } from 'vs/base/common/resources';
|
|
|
|
class NodeModuleRequireInterceptor extends RequireInterceptor {
|
|
|
|
@@ -76,7 +78,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
|
};
|
|
}
|
|
|
|
- protected _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
|
|
+ protected _loadCommonJSModule<T>(module: URI | IExtensionDescription, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
|
|
+ if (!URI.isUri(module)) {
|
|
+ module = joinPath(module.extensionLocation, module.main!);
|
|
+ }
|
|
if (module.scheme !== Schemas.file) {
|
|
throw new Error(`Cannot load URI: '${module}', must be of file-scheme`);
|
|
}
|
|
diff --git a/src/vs/workbench/api/node/extHostStoragePaths.ts b/src/vs/workbench/api/node/extHostStoragePaths.ts
|
|
index afdd6bf398..1633daf93d 100644
|
|
--- a/src/vs/workbench/api/node/extHostStoragePaths.ts
|
|
+++ b/src/vs/workbench/api/node/extHostStoragePaths.ts
|
|
@@ -5,13 +5,14 @@
|
|
|
|
import * as path from 'vs/base/common/path';
|
|
import { URI } from 'vs/base/common/uri';
|
|
-import * as pfs from 'vs/base/node/pfs';
|
|
-import { IEnvironment, IStaticWorkspaceData } from 'vs/workbench/api/common/extHost.protocol';
|
|
+import { IEnvironment, IStaticWorkspaceData, MainContext } from 'vs/workbench/api/common/extHost.protocol';
|
|
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
|
|
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
|
|
import { withNullAsUndefined } from 'vs/base/common/types';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
+import { IExtHostRpcService } from '../common/extHostRpcService';
|
|
+import { VSBuffer } from 'vs/base/common/buffer';
|
|
|
|
export class ExtensionStoragePaths implements IExtensionStoragePaths {
|
|
|
|
@@ -26,6 +27,7 @@ export class ExtensionStoragePaths implements IExtensionStoragePaths {
|
|
constructor(
|
|
@IExtHostInitDataService initData: IExtHostInitDataService,
|
|
@ILogService private readonly _logService: ILogService,
|
|
+ @IExtHostRpcService private readonly _extHostRpc: IExtHostRpcService,
|
|
) {
|
|
this._workspace = withNullAsUndefined(initData.workspace);
|
|
this._environment = initData.environment;
|
|
@@ -54,21 +56,26 @@ export class ExtensionStoragePaths implements IExtensionStoragePaths {
|
|
const storageName = this._workspace.id;
|
|
const storagePath = path.join(this._environment.appSettingsHome.fsPath, 'workspaceStorage', storageName);
|
|
|
|
- const exists = await pfs.dirExists(storagePath);
|
|
-
|
|
- if (exists) {
|
|
+ // NOTE@coder: Use the file system proxy so this will work in the browser.
|
|
+ const fileSystem = this._extHostRpc.getProxy(MainContext.MainThreadFileSystem);
|
|
+ try {
|
|
+ await fileSystem.$stat(URI.file(storagePath));
|
|
return storagePath;
|
|
+ } catch (error) {
|
|
+ // Doesn't exist.
|
|
}
|
|
|
|
try {
|
|
- await pfs.mkdirp(storagePath);
|
|
- await pfs.writeFile(
|
|
- path.join(storagePath, 'meta.json'),
|
|
- JSON.stringify({
|
|
- id: this._workspace.id,
|
|
- configuration: this._workspace.configuration && URI.revive(this._workspace.configuration).toString(),
|
|
- name: this._workspace.name
|
|
- }, undefined, 2)
|
|
+ // NOTE@coder: $writeFile performs a mkdirp.
|
|
+ await fileSystem.$writeFile(
|
|
+ URI.file(path.join(storagePath, 'meta.json')),
|
|
+ VSBuffer.fromString(
|
|
+ JSON.stringify({
|
|
+ id: this._workspace.id,
|
|
+ configuration: this._workspace.configuration && URI.revive(this._workspace.configuration).toString(),
|
|
+ name: this._workspace.name
|
|
+ }, undefined, 2)
|
|
+ )
|
|
);
|
|
return storagePath;
|
|
|
|
diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts
|
|
index 4781f22676..86c9246f51 100644
|
|
--- a/src/vs/workbench/api/worker/extHostExtensionService.ts
|
|
+++ b/src/vs/workbench/api/worker/extHostExtensionService.ts
|
|
@@ -9,6 +9,9 @@ import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHost
|
|
import { endsWith } from 'vs/base/common/strings';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
|
|
+import { joinPath } from 'vs/base/common/resources';
|
|
+import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
|
+import { loadCommonJSModule } from 'vs/server/browser/worker';
|
|
|
|
class WorkerRequireInterceptor extends RequireInterceptor {
|
|
|
|
@@ -41,7 +44,14 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
|
await this._fakeModules.install();
|
|
}
|
|
|
|
- protected async _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
|
|
+ protected async _loadCommonJSModule<T>(module: URI | IExtensionDescription, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
|
|
+ if (!URI.isUri(module) && module.extensionKind !== 'web') {
|
|
+ return loadCommonJSModule(module, activationTimesBuilder, this._nodeProxy, this._logService, this._fakeModules!.getModule('vscode', module.extensionLocation));
|
|
+ }
|
|
+
|
|
+ if (!URI.isUri(module)) {
|
|
+ module = joinPath(module.extensionLocation, module.main!);
|
|
+ }
|
|
|
|
module = module.with({ path: ensureSuffix(module.path, '.js') });
|
|
const response = await fetch(module.toString(true));
|
|
@@ -57,7 +67,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
|
const _exports = {};
|
|
const _module = { exports: _exports };
|
|
const _require = (request: string) => {
|
|
- const result = this._fakeModules!.getModule(request, module);
|
|
+ const result = this._fakeModules!.getModule(request, <URI>module);
|
|
if (result === undefined) {
|
|
throw new Error(`Cannot load module '${request}'`);
|
|
}
|
|
diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts
|
|
index 8973e3fc36..7e3286bd37 100644
|
|
--- a/src/vs/workbench/browser/web.main.ts
|
|
+++ b/src/vs/workbench/browser/web.main.ts
|
|
@@ -49,6 +49,7 @@ import { IndexedDBLogProvider } from 'vs/workbench/services/log/browser/indexedD
|
|
import { InMemoryLogProvider } from 'vs/workbench/services/log/common/inMemoryLogProvider';
|
|
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';
|
|
|
|
@@ -87,6 +88,7 @@ class BrowserMain extends Disposable {
|
|
|
|
// Startup
|
|
workbench.startup();
|
|
+ await initialize(services.serviceCollection);
|
|
}
|
|
|
|
private registerListeners(workbench: Workbench, storageService: BrowserStorageService): void {
|
|
diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts
|
|
index 597dd5d96f..9041a1e81b 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> {
|
|
|
|
@@ -67,7 +68,8 @@ export class ResourceContextKey extends Disposable implements IContextKey<URI> {
|
|
set(value: URI | null) {
|
|
if (!ResourceContextKey._uriEquals(this._resourceKey.get(), value)) {
|
|
this._resourceKey.set(value);
|
|
- this._schemeKey.set(value ? value.scheme : null);
|
|
+ // NOTE@coder: Fixes extensions matching against file schemas.
|
|
+ this._schemeKey.set(value ? (value.scheme === Schemas.vscodeRemote ? Schemas.file : value.scheme) : null);
|
|
this._filenameKey.set(value ? basename(value) : null);
|
|
this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null);
|
|
this._extensionKey.set(value ? extname(value) : null);
|
|
diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js
|
|
index 3fed6a33da..45baca0ad2 100644
|
|
--- a/src/vs/workbench/contrib/webview/browser/pre/main.js
|
|
+++ b/src/vs/workbench/contrib/webview/browser/pre/main.js
|
|
@@ -346,7 +346,8 @@
|
|
if (data.endpoint) {
|
|
try {
|
|
const endpointUrl = new URL(data.endpoint);
|
|
- csp.setAttribute('content', csp.getAttribute('content').replace(/vscode-resource:(?=(\s|;|$))/g, endpointUrl.origin));
|
|
+ // NOTE@coder: Add back the trailing slash so it'll work for sub-paths.
|
|
+ csp.setAttribute('content', csp.getAttribute('content').replace(/vscode-resource:(?=(\s|;|$))/g, endpointUrl.origin + "/"));
|
|
} catch (e) {
|
|
console.error('Could not rewrite csp');
|
|
}
|
|
diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts
|
|
index c94ee4e88c..cce3cf6f13 100644
|
|
--- a/src/vs/workbench/services/environment/browser/environmentService.ts
|
|
+++ b/src/vs/workbench/services/environment/browser/environmentService.ts
|
|
@@ -195,8 +195,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
|
|
|
|
@memoize
|
|
get webviewExternalEndpoint(): string {
|
|
- // TODO: get fallback from product.json
|
|
- return (this.options.webviewEndpoint || 'https://{{uuid}}.vscode-webview-test.com/{{commit}}').replace('{{commit}}', product.commit || '0d728c31ebdf03869d2687d9be0b017667c9ff37');
|
|
+ // NOTE@coder: Modified to work against the current URL.
|
|
+ return `${window.location.origin}${window.location.pathname.replace(/\/+$/, '')}/webview/`;
|
|
}
|
|
|
|
@memoize
|
|
@@ -249,6 +249,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
|
|
installSourcePath!: string;
|
|
|
|
builtinExtensionsPath!: string;
|
|
+ extraExtensionPaths!: string[];
|
|
+ extraBuiltinExtensionPaths!: string[];
|
|
|
|
globalStorageHome!: string;
|
|
workspaceStorageHome!: string;
|
|
diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts
|
|
index 5b6a15e820..0f93c896e2 100644
|
|
--- a/src/vs/workbench/services/extensions/browser/extensionService.ts
|
|
+++ b/src/vs/workbench/services/extensions/browser/extensionService.ts
|
|
@@ -119,6 +119,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
|
|
|
} else {
|
|
// remote: only enabled and none-web'ish extension
|
|
+ localExtensions.push(...remoteEnv.extensions.filter(extension => this._isEnabled(extension) && canExecuteOnWeb(extension, this._productService, this._configService)));
|
|
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !canExecuteOnWeb(extension, this._productService, this._configService));
|
|
this._checkEnableProposedApi(remoteEnv.extensions);
|
|
|
|
diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts
|
|
index 9e8352ac88..22a2d296f9 100644
|
|
--- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts
|
|
+++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts
|
|
@@ -32,7 +32,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 0f35c54431..32fff09b18 100644
|
|
--- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
|
+++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
|
|
@@ -53,12 +53,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]);
|
|
};
|
|
})();
|
|
|
|
@@ -131,8 +132,11 @@ function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {
|
|
|
|
// 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' });
|
|
});
|
|
}
|
|
}
|
|
diff --git a/src/vs/workbench/services/extensions/worker/extHost.services.ts b/src/vs/workbench/services/extensions/worker/extHost.services.ts
|
|
index 9056862945..0785d3391d 100644
|
|
--- a/src/vs/workbench/services/extensions/worker/extHost.services.ts
|
|
+++ b/src/vs/workbench/services/extensions/worker/extHost.services.ts
|
|
@@ -18,9 +18,10 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa
|
|
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
|
|
import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
|
|
import { ExtHostExtensionService } from 'vs/workbench/api/worker/extHostExtensionService';
|
|
-import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService';
|
|
+import { ExtHostNodeProxy, IExtHostNodeProxy } from 'vs/server/browser/extHostNodeProxy';
|
|
+import { ExtensionStoragePaths } from 'vs/workbench/api/node/extHostStoragePaths';
|
|
import { IExtHostTunnelService, ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
|
import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs/workbench/api/common/extHostApiDeprecationService';
|
|
|
|
@@ -36,24 +37,10 @@ registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors);
|
|
registerSingleton(IExtHostStorage, ExtHostStorage);
|
|
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
|
|
registerSingleton(IExtHostSearch, ExtHostSearch);
|
|
+registerSingleton(IExtHostNodeProxy, ExtHostNodeProxy);
|
|
registerSingleton(IExtHostTunnelService, ExtHostTunnelService);
|
|
|
|
-// register services that only throw errors
|
|
-function NotImplementedProxy<T>(name: ServiceIdentifier<T>): { new(): T } {
|
|
- return <any>class {
|
|
- constructor() {
|
|
- return new Proxy({}, {
|
|
- get(target: any, prop: PropertyKey) {
|
|
- if (target[prop]) {
|
|
- return target[prop];
|
|
- }
|
|
- throw new Error(`Not Implemented: ${name}->${String(prop)}`);
|
|
- }
|
|
- });
|
|
- }
|
|
- };
|
|
-}
|
|
registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService);
|
|
registerSingleton(IExtHostTask, WorkerExtHostTask);
|
|
registerSingleton(IExtHostDebugService, WorkerExtHostDebugService);
|
|
-registerSingleton(IExtensionStoragePaths, class extends NotImplementedProxy(IExtensionStoragePaths) { whenReady = Promise.resolve(); });
|
|
+registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);
|
|
diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts
|
|
index 79455414c0..a407593b4d 100644
|
|
--- a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts
|
|
+++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts
|
|
@@ -14,7 +14,11 @@
|
|
|
|
require.config({
|
|
baseUrl: monacoBaseUrl,
|
|
- catchError: true
|
|
+ catchError: true,
|
|
+ paths: {
|
|
+ '@coder/node-browser': `../node_modules/@coder/node-browser/out/client/client.js`,
|
|
+ '@coder/requirefs': `../node_modules/@coder/requirefs/out/requirefs.js`,
|
|
+ }
|
|
});
|
|
|
|
require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err));
|
|
diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts
|
|
index 99394090da..4891e0fece 100644
|
|
--- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts
|
|
+++ b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts
|
|
@@ -5,17 +5,17 @@
|
|
|
|
import { createChannelSender } from 'vs/base/parts/ipc/node/ipc';
|
|
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
|
|
-import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
|
|
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
|
+import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
|
|
|
export class LocalizationsService {
|
|
|
|
_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 a73f3a3e53..7c4ce1acb8 100644
|
|
--- a/src/vs/workbench/workbench.web.main.ts
|
|
+++ b/src/vs/workbench/workbench.web.main.ts
|
|
@@ -34,7 +34,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';
|
|
@@ -119,7 +120,7 @@ import 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.con
|
|
// Issues
|
|
import 'vs/workbench/contrib/issue/browser/issue.contribution';
|
|
|
|
-// Open In Desktop
|
|
-import 'vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution';
|
|
+// Open In Desktop NOTE@coder: remove
|
|
+// import 'vs/workbench/contrib/openInDesktop/browser/openInDesktop.web.contribution';
|
|
|
|
//#endregion
|
|
diff --git a/yarn.lock b/yarn.lock
|
|
index a820c6344a..7e4f410db9 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"
|
|
@@ -5371,6 +5388,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"
|
|
@@ -6729,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"
|