From 822f995357f725216cdee6e05f1f1552ccbbd882 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Wed, 20 Oct 2021 18:42:13 +0200 Subject: [PATCH] Add server folder --- .gitignore | 2 - build/gulpfile.reh.js | 315 ++++- build/gulpfile.vscode.web.js | 213 +++- resources/server/bin-dev/code-web.js | 87 ++ resources/server/bin-dev/code.cmd | 6 + resources/server/bin-dev/code.sh | 18 + resources/server/bin-dev/helpers/browser.cmd | 6 + resources/server/bin-dev/helpers/browser.sh | 18 + resources/server/bin-dev/server.bat | 43 + resources/server/bin-dev/server.sh | 28 + resources/server/bin/code.cmd | 4 + resources/server/bin/code.sh | 12 + resources/server/bin/helpers/browser.cmd | 4 + resources/server/bin/helpers/browser.sh | 12 + resources/server/bin/server.cmd | 24 + resources/server/bin/server.sh | 12 + resources/server/code-192.png | Bin 0 -> 2721 bytes resources/server/code-512.png | Bin 0 -> 2721 bytes resources/server/favicon.ico | Bin 0 -> 34494 bytes resources/server/manifest.json | 19 + .../server/test/test-remote-integration.bat | 79 ++ .../server/test/test-remote-integration.sh | 114 ++ .../server/test/test-web-integration.bat | 55 + resources/server/test/test-web-integration.sh | 36 + resources/server/web-selfhost.bat | 6 + resources/server/web-selfhost.sh | 2 + resources/server/web.bat | 24 + resources/server/web.sh | 26 + src/vs/server/cli.js | 17 + src/vs/server/extensionHostConnection.ts | 272 +++++ src/vs/server/main.js | 156 +++ src/vs/server/remoteAgentEnvironmentImpl.ts | 507 ++++++++ src/vs/server/remoteAgentFileSystemImpl.ts | 305 +++++ src/vs/server/remoteCli.ts | 400 +++++++ src/vs/server/remoteExtensionHostAgent.ts | 64 + src/vs/server/remoteExtensionHostAgentCli.ts | 145 +++ .../server/remoteExtensionHostAgentServer.ts | 1039 +++++++++++++++++ src/vs/server/remoteExtensionHostProcess.ts | 8 + src/vs/server/remoteExtensionManagement.ts | 127 ++ src/vs/server/remoteLanguagePacks.ts | 46 + src/vs/server/remoteTelemetryService.ts | 64 + src/vs/server/remoteTerminalChannel.ts | 332 ++++++ src/vs/server/remoteUriTransformer.ts | 15 + src/vs/server/serverEnvironmentService.ts | 126 ++ src/vs/server/uriTransformer.js | 49 + src/vs/server/webClientServer.ts | 353 ++++++ 46 files changed, 5165 insertions(+), 25 deletions(-) create mode 100644 resources/server/bin-dev/code-web.js create mode 100644 resources/server/bin-dev/code.cmd create mode 100755 resources/server/bin-dev/code.sh create mode 100644 resources/server/bin-dev/helpers/browser.cmd create mode 100755 resources/server/bin-dev/helpers/browser.sh create mode 100644 resources/server/bin-dev/server.bat create mode 100755 resources/server/bin-dev/server.sh create mode 100644 resources/server/bin/code.cmd create mode 100644 resources/server/bin/code.sh create mode 100644 resources/server/bin/helpers/browser.cmd create mode 100644 resources/server/bin/helpers/browser.sh create mode 100644 resources/server/bin/server.cmd create mode 100644 resources/server/bin/server.sh create mode 100644 resources/server/code-192.png create mode 100644 resources/server/code-512.png create mode 100644 resources/server/favicon.ico create mode 100644 resources/server/manifest.json create mode 100644 resources/server/test/test-remote-integration.bat create mode 100755 resources/server/test/test-remote-integration.sh create mode 100644 resources/server/test/test-web-integration.bat create mode 100755 resources/server/test/test-web-integration.sh create mode 100644 resources/server/web-selfhost.bat create mode 100755 resources/server/web-selfhost.sh create mode 100644 resources/server/web.bat create mode 100755 resources/server/web.sh create mode 100644 src/vs/server/cli.js create mode 100644 src/vs/server/extensionHostConnection.ts create mode 100644 src/vs/server/main.js create mode 100644 src/vs/server/remoteAgentEnvironmentImpl.ts create mode 100644 src/vs/server/remoteAgentFileSystemImpl.ts create mode 100644 src/vs/server/remoteCli.ts create mode 100644 src/vs/server/remoteExtensionHostAgent.ts create mode 100644 src/vs/server/remoteExtensionHostAgentCli.ts create mode 100644 src/vs/server/remoteExtensionHostAgentServer.ts create mode 100644 src/vs/server/remoteExtensionHostProcess.ts create mode 100644 src/vs/server/remoteExtensionManagement.ts create mode 100644 src/vs/server/remoteLanguagePacks.ts create mode 100644 src/vs/server/remoteTelemetryService.ts create mode 100644 src/vs/server/remoteTerminalChannel.ts create mode 100644 src/vs/server/remoteUriTransformer.ts create mode 100644 src/vs/server/serverEnvironmentService.ts create mode 100644 src/vs/server/uriTransformer.js create mode 100644 src/vs/server/webClientServer.ts diff --git a/.gitignore b/.gitignore index 95f843584c0e4..738d6793fa9d5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ node_modules/ extensions/**/dist/ /out*/ /extensions/**/out/ -src/vs/server -resources/server build/node_modules coverage/ test_data/ diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index c6909a4e28b49..f21acee97844a 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -6,43 +6,128 @@ 'use strict'; const gulp = require('gulp'); - const path = require('path'); const es = require('event-stream'); const util = require('./lib/util'); const task = require('./lib/task'); +const common = require('./lib/optimize'); +const product = require('../product.json'); +const rename = require('gulp-rename'); +const replace = require('gulp-replace'); +const filter = require('gulp-filter'); +const _ = require('underscore'); +const { getProductionDependencies } = require('./lib/dependencies'); const vfs = require('vinyl-fs'); +const packageJson = require('../package.json'); const flatmap = require('gulp-flatmap'); const gunzip = require('gulp-gunzip'); const File = require('vinyl'); const fs = require('fs'); -const rename = require('gulp-rename'); -const filter = require('gulp-filter'); +const glob = require('glob'); +const { compileBuildTask } = require('./gulpfile.compile'); +const { compileExtensionsBuildTask } = require('./gulpfile.extensions'); +const { vscodeWebEntryPoints, vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } = require('./gulpfile.vscode.web'); const cp = require('child_process'); const REPO_ROOT = path.dirname(__dirname); +const commit = util.getVersion(REPO_ROOT); +const BUILD_ROOT = path.dirname(REPO_ROOT); +const REMOTE_FOLDER = path.join(REPO_ROOT, 'remote'); + +// Targets const BUILD_TARGETS = [ - { platform: 'win32', arch: 'ia32', pkgTarget: 'node8-win-x86' }, - { platform: 'win32', arch: 'x64', pkgTarget: 'node8-win-x64' }, - { platform: 'darwin', arch: null, pkgTarget: 'node8-macos-x64' }, - { platform: 'linux', arch: 'ia32', pkgTarget: 'node8-linux-x86' }, - { platform: 'linux', arch: 'x64', pkgTarget: 'node8-linux-x64' }, - { platform: 'linux', arch: 'armhf', pkgTarget: 'node8-linux-armv7' }, - { platform: 'linux', arch: 'arm64', pkgTarget: 'node8-linux-arm64' }, - { platform: 'alpine', arch: 'arm64', pkgTarget: 'node8-alpine-arm64' }, + { platform: 'win32', arch: 'ia32' }, + { platform: 'win32', arch: 'x64' }, + { platform: 'darwin', arch: null }, + { platform: 'linux', arch: 'ia32' }, + { platform: 'linux', arch: 'x64' }, + { platform: 'linux', arch: 'armhf' }, + { platform: 'linux', arch: 'arm64' }, + { platform: 'alpine', arch: 'arm64' }, // legacy: we use to ship only one alpine so it was put in the arch, but now we ship // multiple alpine images and moved to a better model (alpine as the platform) - { platform: 'linux', arch: 'alpine', pkgTarget: 'node8-linux-alpine' }, + { platform: 'linux', arch: 'alpine' }, ]; -const noop = () => { return Promise.resolve(); }; +const serverResources = [ -BUILD_TARGETS.forEach(({ platform, arch }) => { - for (const target of ['reh', 'reh-web']) { - gulp.task(`vscode-${target}-${platform}${arch ? `-${arch}` : ''}-min`, noop); + // Bootstrap + 'out-build/bootstrap.js', + 'out-build/bootstrap-fork.js', + 'out-build/bootstrap-amd.js', + 'out-build/bootstrap-node.js', + 'out-build/paths.js', + + // Performance + 'out-build/vs/base/common/performance.js', + + // main entry points + 'out-build/vs/server/cli.js', + 'out-build/vs/server/main.js', + + // Watcher + 'out-build/vs/platform/files/**/*.exe', + 'out-build/vs/platform/files/**/*.md', + + // Uri transformer + 'out-build/vs/server/uriTransformer.js', + + // Process monitor + 'out-build/vs/base/node/cpuUsage.sh', + 'out-build/vs/base/node/ps.sh', + + '!**/test/**' +]; + +const serverWithWebResources = [ + + // Include all of server... + ...serverResources, + + // ...and all of web + ...vscodeWebResourceIncludes +]; + +const serverEntryPoints = [ + { + name: 'vs/server/remoteExtensionHostAgent', + exclude: ['vs/css', 'vs/nls'] + }, + { + name: 'vs/server/remoteCli', + exclude: ['vs/css', 'vs/nls'] + }, + { + name: 'vs/server/remoteExtensionHostProcess', + exclude: ['vs/css', 'vs/nls'] + }, + { + name: 'vs/platform/files/node/watcher/unix/watcherApp', + exclude: ['vs/css', 'vs/nls'] + }, + { + name: 'vs/platform/files/node/watcher/nsfw/watcherApp', + exclude: ['vs/css', 'vs/nls'] + }, + { + name: 'vs/platform/files/node/watcher/parcel/watcherApp', + exclude: ['vs/css', 'vs/nls'] + }, + { + name: 'vs/platform/terminal/node/ptyHostMain', + exclude: ['vs/css', 'vs/nls'] } -}); +]; + +const serverWithWebEntryPoints = [ + + // Include all of server + ...serverEntryPoints, + + // Include workbench web + ...vscodeWebEntryPoints +]; function getNodeVersion() { const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); @@ -112,6 +197,202 @@ function nodejs(platform, arch) { .pipe(rename('node')); } +function packageTask(type, platform, arch, sourceFolderName, destinationFolderName) { + const destination = path.join(BUILD_ROOT, destinationFolderName); + + return () => { + const json = require('gulp-json-editor'); + + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })) + .pipe(util.setExecutableBit(['**/*.sh'])) + .pipe(filter(['**', '!**/*.js.map'])); + + const workspaceExtensionPoints = ['debuggers', 'jsonValidation']; + const isUIExtension = (manifest) => { + switch (manifest.extensionKind) { + case 'ui': return true; + case 'workspace': return false; + default: { + if (manifest.main) { + return false; + } + if (manifest.contributes && Object.keys(manifest.contributes).some(key => workspaceExtensionPoints.indexOf(key) !== -1)) { + return false; + } + // Default is UI Extension + return true; + } + } + }; + const localWorkspaceExtensions = glob.sync('extensions/*/package.json') + .filter((extensionPath) => { + if (type === 'reh-web') { + return true; // web: ship all extensions for now + } + + const manifest = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, extensionPath)).toString()); + return !isUIExtension(manifest); + }).map((extensionPath) => path.basename(path.dirname(extensionPath))) + .filter(name => name !== 'vscode-api-tests' && name !== 'vscode-test-resolver'); // Do not ship the test extensions + const marketplaceExtensions = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'product.json'), 'utf8')).builtInExtensions + .filter(entry => !entry.platforms || new Set(entry.platforms).has(platform)) + .filter(entry => !entry.clientOnly) + .map(entry => entry.name); + const extensionPaths = [...localWorkspaceExtensions, ...marketplaceExtensions] + .map(name => `.build/extensions/${name}/**`); + + const extensions = gulp.src(extensionPaths, { base: '.build', dot: true }); + const extensionsCommonDependencies = gulp.src('.build/extensions/node_modules/**', { base: '.build', dot: true }); + const sources = es.merge(src, extensions, extensionsCommonDependencies) + .pipe(filter(['**', '!**/*.js.map'], { dot: true })); + + let version = packageJson.version; + const quality = product.quality; + + if (quality && quality !== 'stable') { + version += '-' + quality; + } + + const name = product.nameShort; + const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) + .pipe(json({ name, version })); + + const date = new Date().toISOString(); + + const productJsonStream = gulp.src(['product.json'], { base: '.' }) + .pipe(json({ commit, date })); + + const license = gulp.src(['remote/LICENSE'], { base: 'remote' }); + + const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); + + const productionDependencies = getProductionDependencies(REMOTE_FOLDER); + const dependenciesSrc = _.flatten(productionDependencies.map(d => path.relative(REPO_ROOT, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`])); + const deps = gulp.src(dependenciesSrc, { base: 'remote', dot: true }) + // filter out unnecessary files, no source maps in server build + .pipe(filter(['**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.js.map'])) + .pipe(util.cleanNodeModules(path.join(__dirname, '.moduleignore'))) + .pipe(jsFilter) + .pipe(util.stripSourceMappingURL()) + .pipe(jsFilter.restore); + + const nodePath = `.build/node/v${nodeVersion}/${platform}-${platform === 'darwin' ? 'x64' : arch}`; + const node = gulp.src(`${nodePath}/**`, { base: nodePath, dot: true }); + + let web = []; + if (type === 'reh-web') { + web = [ + 'resources/server/favicon.ico', + 'resources/server/code-192.png', + 'resources/server/code-512.png', + 'resources/server/manifest.json' + ].map(resource => gulp.src(resource, { base: '.' }).pipe(rename(resource))); + } + + let all = es.merge( + packageJsonStream, + productJsonStream, + license, + sources, + deps, + node, + ...web + ); + + let result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + if (platform === 'win32') { + result = es.merge(result, + gulp.src('resources/server/bin/code.cmd', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/${product.applicationName}.cmd`)), + gulp.src('resources/server/bin/helpers/browser.cmd', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/helpers/browser.cmd`)), + gulp.src('resources/server/bin/server.cmd', { base: '.' }) + .pipe(rename(`server.cmd`)) + ); + } else if (platform === 'linux' || platform === 'alpine' || platform === 'darwin') { + result = es.merge(result, + gulp.src('resources/server/bin/code.sh', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/${product.applicationName}`)) + .pipe(util.setExecutableBit()), + gulp.src('resources/server/bin/helpers/browser.sh', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/helpers/browser.sh`)) + .pipe(util.setExecutableBit()), + gulp.src('resources/server/bin/server.sh', { base: '.' }) + .pipe(rename(`server.sh`)) + .pipe(util.setExecutableBit()) + ); + } + + return result.pipe(vfs.dest(destination)); + }; +} + +['reh', 'reh-web'].forEach(type => { + const optimizeTask = task.define(`optimize-vscode-${type}`, task.series( + util.rimraf(`out-vscode-${type}`), + common.optimizeTask({ + src: 'out-build', + entryPoints: _.flatten(type === 'reh' ? serverEntryPoints : serverWithWebEntryPoints), + otherSources: [], + resources: type === 'reh' ? serverResources : serverWithWebResources, + loaderConfig: common.loaderConfig(), + out: `out-vscode-${type}`, + inlineAmdImages: true, + bundleInfo: undefined, + fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions') + }) + )); + + const minifyTask = task.define(`minify-vscode-${type}`, task.series( + optimizeTask, + util.rimraf(`out-vscode-${type}-min`), + common.minifyTask(`out-vscode-${type}`, `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + )); + gulp.task(minifyTask); + + BUILD_TARGETS.forEach(buildTarget => { + const dashed = (str) => (str ? `-${str}` : ``); + const platform = buildTarget.platform; + const arch = buildTarget.arch; + + ['', 'min'].forEach(minified => { + const sourceFolderName = `out-vscode-${type}${dashed(minified)}`; + const destinationFolderName = `vscode-${type}${dashed(platform)}${dashed(arch)}`; + + const serverTaskCI = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series( + gulp.task(`node-${platform}-${platform === 'darwin' ? 'x64' : arch}`), + util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), + packageTask(type, platform, arch, sourceFolderName, destinationFolderName) + )); + gulp.task(serverTaskCI); + + const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( + compileBuildTask, + compileExtensionsBuildTask, + minified ? minifyTask : optimizeTask, + serverTaskCI + )); + gulp.task(serverTask); + }); + }); +}); + function mixinServer(watch) { const packageJSONPath = path.join(path.dirname(__dirname), 'package.json'); function exec(cmdLine) { diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 4172ea38c4a22..2775330ccd801 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -6,11 +6,212 @@ 'use strict'; const gulp = require('gulp'); +const path = require('path'); +const es = require('event-stream'); +const util = require('./lib/util'); +const task = require('./lib/task'); +const common = require('./lib/optimize'); +const product = require('../product.json'); +const rename = require('gulp-rename'); +const filter = require('gulp-filter'); +const _ = require('underscore'); +const { getProductionDependencies } = require('./lib/dependencies'); +const vfs = require('vinyl-fs'); +const fs = require('fs'); +const packageJson = require('../package.json'); +const { compileBuildTask } = require('./gulpfile.compile'); +const extensions = require('./lib/extensions'); -const noop = () => { return Promise.resolve(); }; +const REPO_ROOT = path.dirname(__dirname); +const BUILD_ROOT = path.dirname(REPO_ROOT); +const WEB_FOLDER = path.join(REPO_ROOT, 'remote', 'web'); -gulp.task('minify-vscode-web', noop); -gulp.task('vscode-web', noop); -gulp.task('vscode-web-min', noop); -gulp.task('vscode-web-ci', noop); -gulp.task('vscode-web-min-ci', noop); +const commit = util.getVersion(REPO_ROOT); +const quality = product.quality; +const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; + +const vscodeWebResourceIncludes = [ + // Workbench + 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg}', + 'out-build/vs/code/browser/workbench/*.html', + 'out-build/vs/base/browser/ui/codicons/codicon/**/*.ttf', + 'out-build/vs/**/markdown.css', + + // Webview + 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', + 'out-build/vs/workbench/contrib/webview/browser/pre/*.html', + + // Extension Worker + 'out-build/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html', + 'out-build/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html', + + // Web node paths (needed for integration tests) + 'out-build/vs/webPackagePaths.js', +]; +exports.vscodeWebResourceIncludes = vscodeWebResourceIncludes; + +const vscodeWebResources = [ + + // Includes + ...vscodeWebResourceIncludes, + + // Excludes + '!out-build/vs/**/{node,electron-browser,electron-main}/**', + '!out-build/vs/editor/standalone/**', + '!out-build/vs/workbench/**/*-tb.png', + '!**/test/**' +]; + +const buildfile = require('../src/buildfile'); + +const vscodeWebEntryPoints = _.flatten([ + buildfile.entrypoint('vs/workbench/workbench.web.api'), + buildfile.base, + buildfile.workerExtensionHost, + buildfile.workerNotebook, + buildfile.workerLanguageDetection, + buildfile.workerLocalFileSearch, + buildfile.keyboardMaps, + buildfile.workbenchWeb +]); +exports.vscodeWebEntryPoints = vscodeWebEntryPoints; + +const buildDate = new Date().toISOString(); + +/** + * @param extensionsRoot {string} The location where extension will be read from + */ +const createVSCodeWebFileContentMapper = (extensionsRoot) => { + /** + * @param content {string} The contens of the file + * @param path {string} The absolute file path, always using `/`, even on Windows + */ + const result = (content, path) => { + // (1) Patch product configuration + if (path.endsWith('vs/platform/product/common/product.js')) { + const productConfiguration = JSON.stringify({ + ...product, + extensionAllowedProposedApi: [...product.extensionAllowedProposedApi], + version, + commit, + date: buildDate + }); + return content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', productConfiguration.substr(1, productConfiguration.length - 2) /* without { and }*/); + } + + // (2) Patch builtin extensions + if (path.endsWith('vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.js')) { + // Do not inline `vscode-web-playground` even if it has been packed! + const builtinExtensions = JSON.stringify(extensions.scanBuiltinExtensions(extensionsRoot, ['vscode-web-playground'])); + return content.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', builtinExtensions.substr(1, builtinExtensions.length - 2) /* without [ and ]*/); + } + + return content; + }; + return result; +}; +exports.createVSCodeWebFileContentMapper = createVSCodeWebFileContentMapper; + +const optimizeVSCodeWebTask = task.define('optimize-vscode-web', task.series( + util.rimraf('out-vscode-web'), + common.optimizeTask({ + src: 'out-build', + entryPoints: _.flatten(vscodeWebEntryPoints), + otherSources: [], + resources: vscodeWebResources, + loaderConfig: common.loaderConfig(), + externalLoaderInfo: util.createExternalLoaderConfig(product.webEndpointUrl, commit, quality), + out: 'out-vscode-web', + inlineAmdImages: true, + bundleInfo: undefined, + fileContentMapper: createVSCodeWebFileContentMapper('.build/web/extensions') + }) +)); + +const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( + optimizeVSCodeWebTask, + util.rimraf('out-vscode-web-min'), + common.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) +)); +gulp.task(minifyVSCodeWebTask); + +function packageTask(sourceFolderName, destinationFolderName) { + const destination = path.join(BUILD_ROOT, destinationFolderName); + + return () => { + const json = require('gulp-json-editor'); + + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })); + + const extensions = gulp.src('.build/web/extensions/**', { base: '.build/web', dot: true }); + + const sources = es.merge(src, extensions) + .pipe(filter(['**', '!**/*.js.map'], { dot: true })); + + const name = product.nameShort; + const packageJsonStream = gulp.src(['remote/web/package.json'], { base: 'remote/web' }) + .pipe(json({ name, version })); + + const license = gulp.src(['remote/LICENSE'], { base: 'remote' }); + + const productionDependencies = getProductionDependencies(WEB_FOLDER); + const dependenciesSrc = _.flatten(productionDependencies.map(d => path.relative(REPO_ROOT, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`])); + + const deps = gulp.src(dependenciesSrc, { base: 'remote/web', dot: true }) + .pipe(filter(['**', '!**/package-lock.json'])) + .pipe(util.cleanNodeModules(path.join(__dirname, '.webignore'))); + + const favicon = gulp.src('resources/server/favicon.ico', { base: 'resources/server' }); + const manifest = gulp.src('resources/server/manifest.json', { base: 'resources/server' }); + const pwaicons = es.merge( + gulp.src('resources/server/code-192.png', { base: 'resources/server' }), + gulp.src('resources/server/code-512.png', { base: 'resources/server' }) + ); + + let all = es.merge( + packageJsonStream, + license, + sources, + deps, + favicon, + manifest, + pwaicons + ); + + let result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + return result.pipe(vfs.dest(destination)); + }; +} + +const compileWebExtensionsBuildTask = task.define('compile-web-extensions-build', task.series( + task.define('clean-web-extensions-build', util.rimraf('.build/web/extensions')), + task.define('bundle-web-extensions-build', () => extensions.packageLocalExtensionsStream(true).pipe(gulp.dest('.build/web'))), + task.define('bundle-marketplace-web-extensions-build', () => extensions.packageMarketplaceExtensionsStream(true).pipe(gulp.dest('.build/web'))), + task.define('bundle-web-extension-media-build', () => extensions.buildExtensionMedia(false, '.build/web/extensions')), +)); +gulp.task(compileWebExtensionsBuildTask); + +const dashed = (str) => (str ? `-${str}` : ``); + +['', 'min'].forEach(minified => { + const sourceFolderName = `out-vscode-web${dashed(minified)}`; + const destinationFolderName = `vscode-web`; + + const vscodeWebTaskCI = task.define(`vscode-web${dashed(minified)}-ci`, task.series( + compileWebExtensionsBuildTask, + minified ? minifyVSCodeWebTask : optimizeVSCodeWebTask, + util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), + packageTask(sourceFolderName, destinationFolderName) + )); + gulp.task(vscodeWebTaskCI); + + const vscodeWebTask = task.define(`vscode-web${dashed(minified)}`, task.series( + compileBuildTask, + vscodeWebTaskCI + )); + gulp.task(vscodeWebTask); +}); diff --git a/resources/server/bin-dev/code-web.js b/resources/server/bin-dev/code-web.js new file mode 100644 index 0000000000000..d5276fb41d0f3 --- /dev/null +++ b/resources/server/bin-dev/code-web.js @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +const cp = require('child_process'); +const path = require('path'); +const os = require('os'); + +const serverArgs = []; + +// Server Config +let PORT = 9888; +let DRIVER = undefined; +let LOGS_PATH = undefined; + +// Workspace Config +let FOLDER = undefined; +let WORKSPACE = undefined; + +// Settings Sync Config +let GITHUB_AUTH_TOKEN = undefined; +let ENABLE_SYNC = false; + +for (let idx = 0; idx <= process.argv.length - 2; idx++) { + const arg = process.argv[idx]; + switch (arg) { + case '--port': PORT = Number(process.argv[idx + 1]); break; + case '--folder': FOLDER = process.argv[idx + 1]; break; + case '--workspace': WORKSPACE = process.argv[idx + 1]; break; + case '--driver': DRIVER = process.argv[idx + 1]; break; + case '--github-auth': GITHUB_AUTH_TOKEN = process.argv[idx + 1]; break; + case '--logsPath': LOGS_PATH = process.argv[idx + 1]; break; + case '--enable-sync': ENABLE_SYNC = true; break; + } +} + +serverArgs.push('--port', String(PORT)); +if (FOLDER) { + serverArgs.push('--folder', FOLDER); +} +if (WORKSPACE) { + serverArgs.push('--workspace', WORKSPACE); +} +if (DRIVER) { + serverArgs.push('--driver', DRIVER); + + // given a DRIVER, we auto-shutdown when tests are done + serverArgs.push('--enable-remote-auto-shutdown', '--remote-auto-shutdown-without-delay'); +} +if (LOGS_PATH) { + serverArgs.push('--logsPath', LOGS_PATH); +} +if (GITHUB_AUTH_TOKEN) { + serverArgs.push('--github-auth', GITHUB_AUTH_TOKEN); +} +if (ENABLE_SYNC) { + serverArgs.push('--enable-sync', true); +} + +// Connection Token +serverArgs.push('--connectionToken', '00000'); + +// Server should really only listen from localhost +serverArgs.push('--host', '127.0.0.1'); + +const env = { ...process.env }; +env['VSCODE_AGENT_FOLDER'] = env['VSCODE_AGENT_FOLDER'] || path.join(os.homedir(), '.vscode-web-dev'); +const entryPoint = path.join(__dirname, '..', '..', '..', 'out', 'vs', 'server', 'main.js'); + +startServer(); + +function startServer() { + const proc = cp.spawn(process.execPath, [entryPoint, ...serverArgs], { env }); + + proc.stdout.on('data', data => { + // Log everything + console.log(data.toString()); + }); + + // Log errors + proc.stderr.on('data', data => { + console.error(data.toString()); + }); +} diff --git a/resources/server/bin-dev/code.cmd b/resources/server/bin-dev/code.cmd new file mode 100644 index 0000000000000..ac906785045a5 --- /dev/null +++ b/resources/server/bin-dev/code.cmd @@ -0,0 +1,6 @@ +@echo off +setlocal +SET VSCODE_PATH=%~dp0..\..\.. +FOR /F "tokens=* USEBACKQ" %%g IN (`where /r "%VSCODE_PATH%\.build\node" node.exe`) do (SET "NODE=%%g") +call "%NODE%" "%VSCODE_PATH%\out\vs\server\cli.js" "Code Server - Dev" "" "" "code.cmd" %* +endlocal diff --git a/resources/server/bin-dev/code.sh b/resources/server/bin-dev/code.sh new file mode 100755 index 0000000000000..61e57cb7abe35 --- /dev/null +++ b/resources/server/bin-dev/code.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) +else + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) +fi + +PROD_NAME="Code Server - Dev" +VERSION="" +COMMIT="" +EXEC_NAME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" +CLI_SCRIPT="$VSCODE_PATH/out/vs/server/cli.js" +node "$CLI_SCRIPT" "$PROD_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "$@" diff --git a/resources/server/bin-dev/helpers/browser.cmd b/resources/server/bin-dev/helpers/browser.cmd new file mode 100644 index 0000000000000..4f195ce7ec88b --- /dev/null +++ b/resources/server/bin-dev/helpers/browser.cmd @@ -0,0 +1,6 @@ +@echo off +setlocal +SET VSCODE_PATH=%~dp0..\..\..\.. +FOR /F "tokens=* USEBACKQ" %%g IN (`where /r "%VSCODE_PATH%\.build\node" node.exe`) do (SET "NODE=%%g") +call "%NODE%" "%VSCODE_PATH%\out\vs\server\cli.js" "Code Server - Dev" "" "" "code.cmd" "--openExternal" %* +endlocal diff --git a/resources/server/bin-dev/helpers/browser.sh b/resources/server/bin-dev/helpers/browser.sh new file mode 100755 index 0000000000000..60ff85d6e77a4 --- /dev/null +++ b/resources/server/bin-dev/helpers/browser.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(dirname $(realpath "$0")))))) +else + VSCODE_PATH=$(dirname $(dirname $(dirname $(dirname $(dirname $(readlink -f $0)))))) +fi + +PROD_NAME="Code Server - Dev" +VERSION="" +COMMIT="" +EXEC_NAME="" +CLI_SCRIPT="$VSCODE_PATH/out/vs/server/cli.js" +node "$CLI_SCRIPT" "$PROD_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "--openExternal" "$@" diff --git a/resources/server/bin-dev/server.bat b/resources/server/bin-dev/server.bat new file mode 100644 index 0000000000000..d6a61b53267b1 --- /dev/null +++ b/resources/server/bin-dev/server.bat @@ -0,0 +1,43 @@ +@echo off +setlocal + +title VSCode Remote Agent + +pushd %~dp0\..\..\.. + +:: Configuration +set NODE_ENV=development +set VSCODE_DEV=1 + +:: Sync built-in extensions +call yarn download-builtin-extensions + +FOR /F "tokens=*" %%g IN ('node build/lib/node.js') do (SET NODE=%%g) + +:: Download nodejs executable for remote +IF NOT EXIST "%NODE%" ( + call yarn gulp node +) + +:: Launch Agent +set _FIRST_ARG=%1 +if "%_FIRST_ARG:~0,9%"=="--inspect" ( + set INSPECT=%1 + shift +) else ( + set INSPECT= +) + +:loop1 +if "%~1"=="" goto after_loop +set RESTVAR=%RESTVAR% %1 +shift +goto loop1 + +:after_loop + +call "%NODE%" %INSPECT% "out\vs\server\main.js" %RESTVAR% + +popd + +endlocal diff --git a/resources/server/bin-dev/server.sh b/resources/server/bin-dev/server.sh new file mode 100755 index 0000000000000..99a47c4fb2212 --- /dev/null +++ b/resources/server/bin-dev/server.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) +else + ROOT=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) +fi + +function code() { + cd $ROOT + + # Sync built-in extensions + yarn download-builtin-extensions + + NODE=$(node build/lib/node.js) + + # Download nodejs + if [ ! -f $NODE ]; then + yarn gulp node + fi + + NODE_ENV=development \ + VSCODE_DEV=1 \ + $NODE "$ROOT/out/vs/server/main.js" "$@" +} + +code "$@" diff --git a/resources/server/bin/code.cmd b/resources/server/bin/code.cmd new file mode 100644 index 0000000000000..0cbff2f7c25b2 --- /dev/null +++ b/resources/server/bin/code.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +call "%~dp0..\node" "%~dp0..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" %* +endlocal \ No newline at end of file diff --git a/resources/server/bin/code.sh b/resources/server/bin/code.sh new file mode 100644 index 0000000000000..2fadda2f2bae2 --- /dev/null +++ b/resources/server/bin/code.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +ROOT=$(dirname "$(dirname "$0")") + +APP_NAME="@@APPNAME@@" +VERSION="@@VERSION@@" +COMMIT="@@COMMIT@@" +EXEC_NAME="@@APPNAME@@" +CLI_SCRIPT="$ROOT/out/vs/server/cli.js" +"$ROOT/node" "$CLI_SCRIPT" "$APP_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "$@" diff --git a/resources/server/bin/helpers/browser.cmd b/resources/server/bin/helpers/browser.cmd new file mode 100644 index 0000000000000..33625f17d455f --- /dev/null +++ b/resources/server/bin/helpers/browser.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +call "%~dp0..\..\node" "%~dp0..\..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" "--openExternal" %* +endlocal diff --git a/resources/server/bin/helpers/browser.sh b/resources/server/bin/helpers/browser.sh new file mode 100644 index 0000000000000..2cc3570be9672 --- /dev/null +++ b/resources/server/bin/helpers/browser.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# +ROOT=$(dirname "$(dirname "$(dirname "$0")")") + +APP_NAME="@@APPNAME@@" +VERSION="@@VERSION@@" +COMMIT="@@COMMIT@@" +EXEC_NAME="@@APPNAME@@" +CLI_SCRIPT="$ROOT/out/vs/server/cli.js" +"$ROOT/node" "$CLI_SCRIPT" "$APP_NAME" "$VERSION" "$COMMIT" "$EXEC_NAME" "--openExternal" "$@" diff --git a/resources/server/bin/server.cmd b/resources/server/bin/server.cmd new file mode 100644 index 0000000000000..3b2c1b7a1e8ac --- /dev/null +++ b/resources/server/bin/server.cmd @@ -0,0 +1,24 @@ +@echo off +setlocal + +set ROOT_DIR=%~dp0 + +set _FIRST_ARG=%1 +if "%_FIRST_ARG:~0,9%"=="--inspect" ( + set INSPECT=%1 + shift +) else ( + set INSPECT= +) + +:loop1 +if "%~1"=="" goto after_loop +set RESTVAR=%RESTVAR% %1 +shift +goto loop1 + +:after_loop + +"%ROOT_DIR%node.exe" %INSPECT% "%ROOT_DIR%out\vs\server\main.js" %RESTVAR% + +endlocal diff --git a/resources/server/bin/server.sh b/resources/server/bin/server.sh new file mode 100644 index 0000000000000..66b7ec6381d03 --- /dev/null +++ b/resources/server/bin/server.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# + +case "$1" in + --inspect*) INSPECT="$1"; shift;; +esac + +ROOT="$(dirname "$0")" + +"$ROOT/node" ${INSPECT:-} "$ROOT/out/vs/server/main.js" "$@" diff --git a/resources/server/code-192.png b/resources/server/code-192.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8646f8282c77316dcfde05b9f339678079a5c5 GIT binary patch literal 2721 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7+9ErRIjnw1`sdZ(btiIVPik{pF~z5Um@8e z$d`ekN{xY`p@o6r7f`6-1p`B=0RzLU1O^7H84L{K1#@-<+5jcg1AIbUf%O0X{{v>9 zUvc(%&6e9@Rr|a5-p^fg{ph{-PLofoHXSzZK0fp4V|&MS%YaH5OM?7@862M7NCR<_ zyxmrx$9H+pg(s_~zt{uxnQDn^L`h0wNvc(H zQ7VvPFfuT-&^0vFH82Y?GO#i+wK6i-HZZU0 t8-nxGO3D+9QW+dm@{>{(JaZG%Q-e|yQz{Ejrh?jy44$rjF6*2UngGKW=cNDu literal 0 HcmV?d00001 diff --git a/resources/server/code-512.png b/resources/server/code-512.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8646f8282c77316dcfde05b9f339678079a5c5 GIT binary patch literal 2721 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7+9ErRIjnw1`sdZ(btiIVPik{pF~z5Um@8e z$d`ekN{xY`p@o6r7f`6-1p`B=0RzLU1O^7H84L{K1#@-<+5jcg1AIbUf%O0X{{v>9 zUvc(%&6e9@Rr|a5-p^fg{ph{-PLofoHXSzZK0fp4V|&MS%YaH5OM?7@862M7NCR<_ zyxmrx$9H+pg(s_~zt{uxnQDn^L`h0wNvc(H zQ7VvPFfuT-&^0vFH82Y?GO#i+wK6i-HZZU0 t8-nxGO3D+9QW+dm@{>{(JaZG%Q-e|yQz{Ejrh?jy44$rjF6*2UngGKW=cNDu literal 0 HcmV?d00001 diff --git a/resources/server/favicon.ico b/resources/server/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f9f0de3eb46f9cf6cab16ce698bef2e6896f2dd8 GIT binary patch literal 34494 zcmeHQy^CE%6dyqg8%dc;UZIdOohctCSf{d5NU48-jW%H`QLqW5*CbHre1GRkaR1Z4j~fm#{O@U-bBN)u$L3n1F%zOc+0mB!`rrTb>YCoL zok_>nAMLp9NpGTo{)?kWdw=}!RSQoK5B9XeEpFEFYqaI_xg_2+t2m=WmD1s88NZhN zb<8TA{#iWTTO1u)0rmvA>buH88?l7^#`9--j$n@R@=%=Ol&k$V(TA=)o&m?bz@>kT zeV!cLTAR0sUx?#<(09@BmB+|Wbjpj~=B(KkOmCHf}0qzAeHJnzjk%r)RS za+&Y|II^T=o!5kraftDT6w)?y45OVHCw%h?ZIK{{uU%{%=!MV}{_FPV_fL+S_am2I zQtglD$gV`s#4g?_-&FpvEw?wvhqS-XrZIJ?_9q_LlOP&hx$Sw1`Vc)=?wbGLADY06 z)6eB^+tY)rHozLXZ~mNKEWYXbkHP%0baPA-K!^=7D8vndqEZzry9`}4WmoJxp zZU5Ki4}1FTZPwZDN4fUr{*1*_`G0rgUF)8j`CfqD3HW46gfL_BdQz95|pjQ_T>3k+4sJcSYAU zwEGmVX++YfETCOgb)NRXzm2yPcSCaQRZ_w^gpfXcwn5CL6mn1Du^+Nf!aA1M4~?5^ zG?8EIA3NS;{A}m^#TwrB9@|%D+r#M^PeY9#$P$~NTbI1NUgWore~h2l`z+2Ta{G4S zL)pKJZ(sEyzkU30`hx4u5Lh5Y$Hvp(yFvVWg4`e>Q*!{@R081wxl&Y;Hj&utHxw;_${Fn|an7AS0i z)f}dCLxq`7fE~9l$8k<;uHm|d_M8?9B#8yY0<8t~ytzJcScQ%W|LyCX9XDafH+{af z{Ic$&OU>N@fWDD|%%j#{@0E4hD4~t$zxqa2{4e8Q=70P-Gh0j=B>pGFHk)j`OyXbp z_!sgWe8~P8_hkxR!~ZGo0Qj83`;QcTc^}cg{P-9D$Dcvprty#XmviO=eZRl)&c@Dv z9)oJ)HR4}QeK~K@zkK}XK8X1}82Bp`5GObzAm%oo z?;Ib){GYo2r~drI$7D%-4CB9~zMPlnU;qB!$9HhH{ZzzN?r)Bd!T(c_|DwOFe`3~k zp=~j@`F!X280J4@3VGih+s5_V1f~tfqQ5Cq$!pQS{{6qCzd5fO{>NG+xbV;Er`pf!aID|QScwD4ek;E literal 0 HcmV?d00001 diff --git a/resources/server/manifest.json b/resources/server/manifest.json new file mode 100644 index 0000000000000..38b665c8c3b7c --- /dev/null +++ b/resources/server/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Code - OSS", + "short_name": "Code- OSS", + "start_url": "/", + "lang": "en-US", + "display": "standalone", + "icons": [ + { + "src": "/code-192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/code-512.png", + "type": "image/png", + "sizes": "512x512" + } + ] +} diff --git a/resources/server/test/test-remote-integration.bat b/resources/server/test/test-remote-integration.bat new file mode 100644 index 0000000000000..47ee286f12041 --- /dev/null +++ b/resources/server/test/test-remote-integration.bat @@ -0,0 +1,79 @@ +@echo off +setlocal + +pushd %~dp0\..\..\.. + +IF "%~1" == "" ( + set AUTHORITY=vscode-remote://test+test/ + :: backward to forward slashed + set EXT_PATH=%CD:\=/%/extensions + + :: Download nodejs executable for remote + call yarn gulp node +) else ( + set AUTHORITY=%1 + set EXT_PATH=%2 + set VSCODEUSERDATADIR=%3 +) +IF "%VSCODEUSERDATADIR%" == "" ( + set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% +) + +set REMOTE_VSCODE=%AUTHORITY%%EXT_PATH% +set VSCODECRASHDIR=%~dp0\..\..\..\.build\crashes +set VSCODELOGSDIR=%~dp0\..\..\..\.build\logs\remote-integration-tests +set TESTRESOLVER_DATA_FOLDER=%TMP%\testresolverdatafolder-%RANDOM%-%TIME:~6,5% + +if "%VSCODE_REMOTE_SERVER_PATH%"=="" ( + echo "Using remote server out of sources for integration tests" +) else ( + set TESTRESOLVER_INSTALL_BUILTIN_EXTENSION=ms-vscode.vscode-smoketest-check + echo "Using %VSCODE_REMOTE_SERVER_PATH% as server path" +) + +set API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=%VSCODECRASHDIR% --logsPath=%VSCODELOGSDIR% --no-cached-data --disable-updates --disable-keytar --disable-inspect --disable-workspace-trust --user-data-dir=%VSCODEUSERDATADIR% + +:: Figure out which Electron to use for running tests +if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( + echo "Storing crash reports into '%VSCODECRASHDIR%'." + echo "Storing log files into '%VSCODELOGSDIR%'." + + :: Tests in the extension host running from sources + call .\scripts\code.bat --folder-uri=%REMOTE_VSCODE%/vscode-api-tests/testWorkspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/singlefolder-tests %API_TESTS_EXTRA_ARGS% + if %errorlevel% neq 0 exit /b %errorlevel% + + call .\scripts\code.bat --file-uri=%REMOTE_VSCODE%/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/workspace-tests %API_TESTS_EXTRA_ARGS% + if %errorlevel% neq 0 exit /b %errorlevel% +) else ( + echo "Storing crash reports into '%VSCODECRASHDIR%'." + echo "Storing log files into '%VSCODELOGSDIR%'." + echo "Using %INTEGRATION_TEST_ELECTRON_PATH% as Electron path" + + :: Run from a built: need to compile all test extensions + :: because we run extension tests from their source folders + :: and the build bundles extensions into .build webpacked + call yarn gulp compile-extension:vscode-api-tests^ + compile-extension:vscode-test-resolver + + :: Configuration for more verbose output + set VSCODE_CLI=1 + set ELECTRON_ENABLE_LOGGING=1 + set ELECTRON_ENABLE_STACK_DUMPING=1 + + :: Tests in the extension host running from built version (both client and server) + call "%INTEGRATION_TEST_ELECTRON_PATH%" --folder-uri=%REMOTE_VSCODE%/vscode-api-tests/testWorkspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/singlefolder-tests %API_TESTS_EXTRA_ARGS% --extensions-dir=%EXT_PATH% --enable-proposed-api=vscode.vscode-test-resolver --enable-proposed-api=vscode.vscode-api-tests --enable-proposed-api=vscode.image-preview + if %errorlevel% neq 0 exit /b %errorlevel% + + call "%INTEGRATION_TEST_ELECTRON_PATH%" --file-uri=%REMOTE_VSCODE%/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=%REMOTE_VSCODE%/vscode-api-tests --extensionTestsPath=%REMOTE_VSCODE%/vscode-api-tests/out/workspace-tests %API_TESTS_EXTRA_ARGS% --extensions-dir=%EXT_PATH% --enable-proposed-api=vscode.vscode-test-resolver --enable-proposed-api=vscode.vscode-api-tests --enable-proposed-api=vscode.image-preview + if %errorlevel% neq 0 exit /b %errorlevel% +) + +IF "%3" == "" ( + rmdir /s /q %VSCODEUSERDATADIR% +) + +rmdir /s /q %TESTRESOLVER_DATA_FOLDER% + +popd + +endlocal diff --git a/resources/server/test/test-remote-integration.sh b/resources/server/test/test-remote-integration.sh new file mode 100755 index 0000000000000..09d14d50c959e --- /dev/null +++ b/resources/server/test/test-remote-integration.sh @@ -0,0 +1,114 @@ +#!/bin/bash +set -e + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) + VSCODEUSERDATADIR=`mktemp -d -t 'myuserdatadir'` + TESTRESOLVER_DATA_FOLDER=`mktemp -d -t 'testresolverdatafolder'` +else + ROOT=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) + VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` + TESTRESOLVER_DATA_FOLDER=`mktemp -d 2>/dev/null` + # --disable-dev-shm-usage --use-gl=swiftshader: when run on docker containers where size of /dev/shm + # partition < 64MB which causes OOM failure for chromium compositor that uses the partition for shared memory + LINUX_EXTRA_ARGS="--disable-dev-shm-usage --use-gl=swiftshader" +fi + +cd $ROOT +if [[ "$1" == "" ]]; then + AUTHORITY=vscode-remote://test+test + EXT_PATH=$ROOT/extensions + # Load remote node + yarn gulp node +else + AUTHORITY=$1 + EXT_PATH=$2 + VSCODEUSERDATADIR=${3:-$VSCODEUSERDATADIR} +fi + +export REMOTE_VSCODE=$AUTHORITY$EXT_PATH +VSCODECRASHDIR=$ROOT/.build/crashes +VSCODELOGSDIR=$ROOT/.build/logs/remote-integration-tests + +# Figure out which Electron to use for running tests +if [ -z "$INTEGRATION_TEST_ELECTRON_PATH" ] +then + echo "Storing crash reports into '$VSCODECRASHDIR'." + echo "Storing log files into '$VSCODELOGSDIR'." + + # code.sh makes sure Test Extensions are compiled + INTEGRATION_TEST_ELECTRON_PATH="./scripts/code.sh" + + # No extra arguments when running out of sources + EXTRA_INTEGRATION_TEST_ARGUMENTS="" +else + echo "Storing crash reports into '$VSCODECRASHDIR'." + echo "Storing log files into '$VSCODELOGSDIR'." + echo "Using $INTEGRATION_TEST_ELECTRON_PATH as Electron path for integration tests" + + # Run from a built: need to compile all test extensions + # because we run extension tests from their source folders + # and the build bundles extensions into .build webpacked + yarn gulp compile-extension:vscode-api-tests \ + compile-extension:vscode-test-resolver \ + compile-extension:markdown-language-features \ + compile-extension:typescript-language-features \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension-media + + # Configuration for more verbose output + export VSCODE_CLI=1 + export ELECTRON_ENABLE_STACK_DUMPING=1 + export ELECTRON_ENABLE_LOGGING=1 + + # Running from a build, we need to enable the vscode-test-resolver extension + EXTRA_INTEGRATION_TEST_ARGUMENTS="--extensions-dir=$EXT_PATH --enable-proposed-api=vscode.vscode-test-resolver --enable-proposed-api=vscode.vscode-api-tests --enable-proposed-api=vscode.image-preview --enable-proposed-api=vscode.git" +fi + +if [ -z "$INTEGRATION_TEST_APP_NAME" ]; then + after_suite() { true; } +else + after_suite() { killall $INTEGRATION_TEST_APP_NAME || true; } +fi + +export TESTRESOLVER_DATA_FOLDER=$TESTRESOLVER_DATA_FOLDER + +# Figure out which remote server to use for running tests +if [ -z "$VSCODE_REMOTE_SERVER_PATH" ] +then + echo "Using remote server out of sources for integration tests" +else + echo "Using $VSCODE_REMOTE_SERVER_PATH as server path for integration tests" + export TESTRESOLVER_INSTALL_BUILTIN_EXTENSION='ms-vscode.vscode-smoketest-check' +fi + +# Tests in the extension host + +API_TESTS_DEFAULT_EXTRA_ARGS="--disable-telemetry --skip-welcome --skip-release-notes --crash-reporter-directory=$VSCODECRASHDIR --logsPath=$VSCODELOGSDIR --no-cached-data --disable-updates --disable-keytar --disable-inspect --disable-workspace-trust --user-data-dir=$VSCODEUSERDATADIR" + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/singlefolder-tests $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --file-uri=$REMOTE_VSCODE/vscode-api-tests/testworkspace.code-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/vscode-api-tests --extensionTestsPath=$REMOTE_VSCODE/vscode-api-tests/out/workspace-tests $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/typescript-language-features/test-workspace --enable-proposed-api=vscode.typescript-language-features --extensionDevelopmentPath=$REMOTE_VSCODE/typescript-language-features --extensionTestsPath=$REMOTE_VSCODE/typescript-language-features/out/test/unit $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/markdown-language-features/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/markdown-language-features --extensionTestsPath=$REMOTE_VSCODE/markdown-language-features/out/test $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$REMOTE_VSCODE/emmet/test-workspace --extensionDevelopmentPath=$REMOTE_VSCODE/emmet --extensionTestsPath=$REMOTE_VSCODE/emmet/out/test $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_EXTRA_ARGS --folder-uri=$AUTHORITY$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$REMOTE_VSCODE/git --extensionTestsPath=$REMOTE_VSCODE/git/out/test $API_TESTS_DEFAULT_EXTRA_ARGS $EXTRA_INTEGRATION_TEST_ARGUMENTS +after_suite + +# Clean up +if [[ "$3" == "" ]]; then + rm -rf $VSCODEUSERDATADIR +fi + +rm -rf $TESTRESOLVER_DATA_FOLDER diff --git a/resources/server/test/test-web-integration.bat b/resources/server/test/test-web-integration.bat new file mode 100644 index 0000000000000..d063276409bed --- /dev/null +++ b/resources/server/test/test-web-integration.bat @@ -0,0 +1,55 @@ +@echo off +setlocal + +pushd %~dp0\..\..\.. + +IF "%~1" == "" ( + set AUTHORITY=vscode-remote://test+test/ + :: backward to forward slashed + set EXT_PATH=%CD:\=/%/extensions + + :: Download nodejs executable for remote + call yarn gulp node +) else ( + set AUTHORITY=%1 + set EXT_PATH=%2 +) + +set REMOTE_VSCODE=%AUTHORITY%%EXT_PATH% + +if "%VSCODE_REMOTE_SERVER_PATH%"=="" ( + echo "Using remote server out of sources for integration web tests" +) else ( + echo "Using %VSCODE_REMOTE_SERVER_PATH% as server path for web integration tests" + + :: Run from a built: need to compile all test extensions + :: because we run extension tests from their source folders + :: and the build bundles extensions into .build webpacked + call yarn gulp compile-extension:vscode-api-tests^ + compile-extension:markdown-language-features^ + compile-extension:typescript-language-features^ + compile-extension:emmet^ + compile-extension:git^ + compile-extension-media +) + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=.\extensions\vscode-api-tests --extensionTestsPath=.\extensions\vscode-api-tests\out\singlefolder-tests %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\vscode-api-tests\testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=.\extensions\vscode-api-tests --extensionTestsPath=.\extensions\vscode-api-tests\out\workspace-tests %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\typescript-language-features\test-workspace --extensionDevelopmentPath=.\extensions\typescript-language-features --extensionTestsPath=.\extensions\typescript-language-features\out\test\unit %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\markdown-language-features\test-workspace --extensionDevelopmentPath=.\extensions\markdown-language-features --extensionTestsPath=.\extensions\markdown-language-features\out\test %* +if %errorlevel% neq 0 exit /b %errorlevel% + +call node .\test\integration\browser\out\index.js --workspacePath=.\extensions\emmet\test-workspace --extensionDevelopmentPath=.\extensions\emmet --extensionTestsPath=.\extensions\emmet\out\test %* +if %errorlevel% neq 0 exit /b %errorlevel% + +for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set TEMPDIR=%%i +set GITWORKSPACE=%TEMPDIR%\git-%RANDOM% +mkdir %GITWORKSPACE% +call node .\test\integration\browser\out\index.js --workspacePath=%GITWORKSPACE% --extensionDevelopmentPath=.\extensions\git --extensionTestsPath=.\extensions\git\out\test --enable-proposed-api=vscode.git %* +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/resources/server/test/test-web-integration.sh b/resources/server/test/test-web-integration.sh new file mode 100755 index 0000000000000..8c6962b4244a7 --- /dev/null +++ b/resources/server/test/test-web-integration.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) +else + ROOT=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) +fi + +cd $ROOT + +if [ -z "$VSCODE_REMOTE_SERVER_PATH" ] +then + echo "Using remote server out of sources for integration web tests" +else + echo "Using $VSCODE_REMOTE_SERVER_PATH as server path for web integration tests" + + # Run from a built: need to compile all test extensions + # because we run extension tests from their source folders + # and the build bundles extensions into .build webpacked + yarn gulp compile-extension:vscode-api-tests \ + compile-extension:markdown-language-features \ + compile-extension:typescript-language-features \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension-media +fi + +# Tests in the extension host +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/markdown-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test "$@" +node test/integration/browser/out/index.js --workspacePath $ROOT/extensions/emmet/test-workspace --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test "$@" +node test/integration/browser/out/index.js --workspacePath $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test "$@" diff --git a/resources/server/web-selfhost.bat b/resources/server/web-selfhost.bat new file mode 100644 index 0000000000000..3e23d682380d6 --- /dev/null +++ b/resources/server/web-selfhost.bat @@ -0,0 +1,6 @@ +@echo off +setlocal + +node %~dp0\bin-dev\code-web.js --selfhost %* + +endlocal \ No newline at end of file diff --git a/resources/server/web-selfhost.sh b/resources/server/web-selfhost.sh new file mode 100755 index 0000000000000..9d6505691bafb --- /dev/null +++ b/resources/server/web-selfhost.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +node $(dirname "$0")/bin-dev/code-web.js --selfhost "$@" \ No newline at end of file diff --git a/resources/server/web.bat b/resources/server/web.bat new file mode 100644 index 0000000000000..d131dafffc6f6 --- /dev/null +++ b/resources/server/web.bat @@ -0,0 +1,24 @@ +@echo off +setlocal + +title VSCode Web Server + +pushd %~dp0\..\.. + +:: Configuration +set NODE_ENV=development +set VSCODE_DEV=1 + +:: Sync built-in extensions +call yarn download-builtin-extensions + +:: Download nodejs executable for remote +call yarn gulp node + +:: Launch Server +FOR /F "tokens=*" %%g IN ('node build/lib/node.js') do (SET NODE=%%g) +call "%NODE%" resources\server\bin-dev\code-web.js %* + +popd + +endlocal \ No newline at end of file diff --git a/resources/server/web.sh b/resources/server/web.sh new file mode 100755 index 0000000000000..da072e5f2d0ea --- /dev/null +++ b/resources/server/web.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(realpath "$0")))) +else + ROOT=$(dirname $(dirname $(dirname $(readlink -f $0)))) +fi + +function code() { + cd $ROOT + + # Sync built-in extensions + yarn download-builtin-extensions + + # Load remote node + yarn gulp node + + NODE=$(node build/lib/node.js) + + NODE_ENV=development \ + VSCODE_DEV=1 \ + $NODE $(dirname "$0")/bin-dev/code-web.js "$@" +} + +code "$@" diff --git a/src/vs/server/cli.js b/src/vs/server/cli.js new file mode 100644 index 0000000000000..3c3575ac559f9 --- /dev/null +++ b/src/vs/server/cli.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +const path = require('path'); + +// Keep bootstrap-amd.js from redefining 'fs'. +delete process.env['ELECTRON_RUN_AS_NODE']; + +// Set default remote native node modules path, if unset +process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', '..', '..', 'remote', 'node_modules'); + +require('../../bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); +require('../../bootstrap-amd').load('vs/server/remoteCli'); diff --git a/src/vs/server/extensionHostConnection.ts b/src/vs/server/extensionHostConnection.ts new file mode 100644 index 0000000000000..3f8b75c36490d --- /dev/null +++ b/src/vs/server/extensionHostConnection.ts @@ -0,0 +1,272 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as net from 'net'; +import { getNLSConfiguration } from 'vs/server/remoteLanguagePacks'; +import { uriTransformerPath } from 'vs/server/remoteUriTransformer'; +import { FileAccess } from 'vs/base/common/network'; +import { join, delimiter } from 'vs/base/common/path'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IRemoteConsoleLog } from 'vs/base/common/console'; +import { Emitter, Event } from 'vs/base/common/event'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; +import { removeDangerousEnvVariables } from 'vs/base/node/processes'; + +export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, language: string, isDebug: boolean, environmentService: IServerEnvironmentService, logService: ILogService): Promise { + const nlsConfig = await getNLSConfiguration(language, environmentService.userDataPath); + + let userShellEnv: typeof process.env | undefined = undefined; + try { + userShellEnv = await resolveShellEnv(logService, environmentService.args, process.env); + } catch (error) { + logService.error('ExtensionHostConnection#buildUserEnvironment resolving shell environment failed', error); + userShellEnv = {}; + } + + const binFolder = environmentService.isBuilt ? join(environmentService.appRoot, 'bin') : join(environmentService.appRoot, 'resources', 'server', 'bin-dev'); + const processEnv = process.env; + let PATH = startParamsEnv['PATH'] || (userShellEnv ? userShellEnv['PATH'] : undefined) || processEnv['PATH']; + if (PATH) { + PATH = binFolder + delimiter + PATH; + } else { + PATH = binFolder; + } + + const env: IProcessEnvironment = { + ...processEnv, + ...userShellEnv, + ...{ + VSCODE_LOG_NATIVE: String(isDebug), + VSCODE_AMD_ENTRYPOINT: 'vs/server/remoteExtensionHostProcess', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true', + VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true', + VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true', + VSCODE_LOG_STACK: 'false', + VSCODE_NLS_CONFIG: JSON.stringify(nlsConfig, undefined, 0) + }, + ...startParamsEnv + }; + if (!environmentService.args['without-browser-env-var']) { + env.BROWSER = join(binFolder, 'helpers', isWindows ? 'browser.cmd' : 'browser.sh'); + } + + setCaseInsensitive(env, 'PATH', PATH); + removeNulls(env); + + return env; +} + +class ConnectionData { + constructor( + public readonly socket: net.Socket, + public readonly socketDrain: Promise, + public readonly initialDataChunk: VSBuffer, + public readonly skipWebSocketFrames: boolean, + public readonly permessageDeflate: boolean, + public readonly inflateBytes: VSBuffer, + ) { } + + public toIExtHostSocketMessage(): IExtHostSocketMessage { + return { + type: 'VSCODE_EXTHOST_IPC_SOCKET', + initialDataChunk: (this.initialDataChunk.buffer).toString('base64'), + skipWebSocketFrames: this.skipWebSocketFrames, + permessageDeflate: this.permessageDeflate, + inflateBytes: (this.inflateBytes.buffer).toString('base64'), + }; + } +} + +export class ExtensionHostConnection { + + private _onClose = new Emitter(); + readonly onClose: Event = this._onClose.event; + + private _disposed: boolean; + private _remoteAddress: string; + private _extensionHostProcess: cp.ChildProcess | null; + private _connectionData: ConnectionData | null; + + constructor( + private readonly _environmentService: IServerEnvironmentService, + private readonly _logService: ILogService, + private readonly _reconnectionToken: string, + remoteAddress: string, + socket: NodeSocket | WebSocketNodeSocket, + initialDataChunk: VSBuffer + ) { + this._disposed = false; + this._remoteAddress = remoteAddress; + this._extensionHostProcess = null; + this._connectionData = ExtensionHostConnection._toConnectionData(socket, initialDataChunk); + this._connectionData.socket.pause(); + + this._log(`New connection established.`); + } + + private get _logPrefix(): string { + return `[${this._remoteAddress}][${this._reconnectionToken.substr(0, 8)}][ExtensionHostConnection] `; + } + + private _log(_str: string): void { + this._logService.info(`${this._logPrefix}${_str}`); + } + + private _logError(_str: string): void { + this._logService.error(`${this._logPrefix}${_str}`); + } + + private static _toConnectionData(socket: NodeSocket | WebSocketNodeSocket, initialDataChunk: VSBuffer): ConnectionData { + if (socket instanceof NodeSocket) { + return new ConnectionData(socket.socket, socket.drain(), initialDataChunk, true, false, VSBuffer.alloc(0)); + } else { + return new ConnectionData(socket.socket.socket, socket.drain(), initialDataChunk, false, socket.permessageDeflate, socket.recordedInflateBytes); + } + } + + private async _sendSocketToExtensionHost(extensionHostProcess: cp.ChildProcess, connectionData: ConnectionData): Promise { + // Make sure all outstanding writes have been drained before sending the socket + await connectionData.socketDrain; + const msg = connectionData.toIExtHostSocketMessage(); + extensionHostProcess.send(msg, connectionData.socket); + } + + public shortenReconnectionGraceTimeIfNecessary(): void { + if (!this._extensionHostProcess) { + return; + } + const msg: IExtHostReduceGraceTimeMessage = { + type: 'VSCODE_EXTHOST_IPC_REDUCE_GRACE_TIME' + }; + this._extensionHostProcess.send(msg); + } + + public acceptReconnection(remoteAddress: string, _socket: NodeSocket | WebSocketNodeSocket, initialDataChunk: VSBuffer): void { + this._remoteAddress = remoteAddress; + this._log(`The client has reconnected.`); + const connectionData = ExtensionHostConnection._toConnectionData(_socket, initialDataChunk); + connectionData.socket.pause(); + + if (!this._extensionHostProcess) { + // The extension host didn't even start up yet + this._connectionData = connectionData; + return; + } + + this._sendSocketToExtensionHost(this._extensionHostProcess, connectionData); + } + + private _cleanResources(): void { + if (this._disposed) { + // already called + return; + } + this._disposed = true; + if (this._connectionData) { + this._connectionData.socket.end(); + this._connectionData = null; + } + if (this._extensionHostProcess) { + this._extensionHostProcess.kill(); + this._extensionHostProcess = null; + } + this._onClose.fire(undefined); + } + + public async start(startParams: IRemoteExtensionHostStartParams): Promise { + try { + let execArgv: string[] = []; + if (startParams.port && !(process).pkg) { + execArgv = [`--inspect${startParams.break ? '-brk' : ''}=0.0.0.0:${startParams.port}`]; + } + + const env = await buildUserEnvironment(startParams.env, startParams.language, !!startParams.debugId, this._environmentService, this._logService); + removeDangerousEnvVariables(env); + + const opts = { + env, + execArgv, + silent: true + }; + + // Run Extension Host as fork of current process + const args = ['--type=extensionHost', `--uriTransformerPath=${uriTransformerPath}`]; + const useHostProxy = this._environmentService.args['use-host-proxy']; + if (useHostProxy !== undefined) { + args.push(`--useHostProxy=${useHostProxy}`); + } + this._extensionHostProcess = cp.fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, args, opts); + const pid = this._extensionHostProcess.pid; + this._log(`<${pid}> Launched Extension Host Process.`); + + // Catch all output coming from the extension host process + this._extensionHostProcess.stdout!.setEncoding('utf8'); + this._extensionHostProcess.stderr!.setEncoding('utf8'); + const onStdout = Event.fromNodeEventEmitter(this._extensionHostProcess.stdout!, 'data'); + const onStderr = Event.fromNodeEventEmitter(this._extensionHostProcess.stderr!, 'data'); + onStdout((e) => this._log(`<${pid}> ${e}`)); + onStderr((e) => this._log(`<${pid}> ${e}`)); + + + // Support logging from extension host + this._extensionHostProcess.on('message', msg => { + if (msg && (msg).type === '__$console') { + logRemoteEntry(this._logService, (msg), `${this._logPrefix}<${pid}>`); + } + }); + + // Lifecycle + this._extensionHostProcess.on('error', (err) => { + this._logError(`<${pid}> Extension Host Process had an error`); + this._logService.error(err); + this._cleanResources(); + }); + + this._extensionHostProcess.on('exit', (code: number, signal: string) => { + this._log(`<${pid}> Extension Host Process exited with code: ${code}, signal: ${signal}.`); + this._cleanResources(); + }); + + const messageListener = (msg: IExtHostReadyMessage) => { + if (msg.type === 'VSCODE_EXTHOST_IPC_READY') { + this._extensionHostProcess!.removeListener('message', messageListener); + this._sendSocketToExtensionHost(this._extensionHostProcess!, this._connectionData!); + this._connectionData = null; + } + }; + this._extensionHostProcess.on('message', messageListener); + + } catch (error) { + console.error('ExtensionHostConnection errored'); + if (error) { + console.error(error); + } + } + } +} + +function setCaseInsensitive(env: { [key: string]: unknown }, key: string, value: string): void { + const pathKeys = Object.keys(env).filter(k => k.toLowerCase() === key.toLowerCase()); + const pathKey = pathKeys.length > 0 ? pathKeys[0] : key; + env[pathKey] = value; +} + +function removeNulls(env: { [key: string]: unknown | null }): void { + // Don't delete while iterating the object itself + for (let key of Object.keys(env)) { + if (env[key] === null) { + delete env[key]; + } + } +} diff --git a/src/vs/server/main.js b/src/vs/server/main.js new file mode 100644 index 0000000000000..3e6b8d72907b3 --- /dev/null +++ b/src/vs/server/main.js @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +const perf = require('../base/common/performance'); +const performance = require('perf_hooks').performance; + +perf.mark('code/server/start'); +// @ts-ignore +global.vscodeServerStartTime = performance.now(); + +function start() { + if (process.argv[2] === '--exec') { + process.argv.splice(1, 2); + require(process.argv[1]); + return; + } + + const minimist = require('minimist'); + + // Do a quick parse to determine if a server or the cli needs to be started + const parsedArgs = minimist(process.argv.slice(2), { + boolean: ['start-server', 'list-extensions', 'print-ip-address'], + string: ['install-extension', 'install-builtin-extension', 'uninstall-extension', 'locate-extension', 'socket-path', 'host', 'port'] + }); + + const shouldSpawnCli = ( + !parsedArgs['start-server'] && + (!!parsedArgs['list-extensions'] || !!parsedArgs['install-extension'] || !!parsedArgs['install-builtin-extension'] || !!parsedArgs['uninstall-extension'] || !!parsedArgs['locate-extension']) + ); + + if (shouldSpawnCli) { + loadCode().then((mod) => { + mod.spawnCli(); + }); + return; + } + + /** + * @typedef { import('./remoteExtensionHostAgentServer').IServerAPI } IServerAPI + */ + /** @type {IServerAPI | null} */ + let _remoteExtensionHostAgentServer = null; + /** @type {Promise | null} */ + let _remoteExtensionHostAgentServerPromise = null; + /** @returns {Promise} */ + const getRemoteExtensionHostAgentServer = () => { + if (!_remoteExtensionHostAgentServerPromise) { + _remoteExtensionHostAgentServerPromise = loadCode().then((mod) => mod.createServer(address)); + } + return _remoteExtensionHostAgentServerPromise; + }; + + const http = require('http'); + const os = require('os'); + + let firstRequest = true; + let firstWebSocket = true; + + /** @type {string | import('net').AddressInfo | null} */ + let address = null; + const server = http.createServer(async (req, res) => { + if (firstRequest) { + firstRequest = false; + perf.mark('code/server/firstRequest'); + } + const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer(); + return remoteExtensionHostAgentServer.handleRequest(req, res); + }); + server.on('upgrade', async (req, socket) => { + if (firstWebSocket) { + firstWebSocket = false; + perf.mark('code/server/firstWebSocket'); + } + const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer(); + // @ts-ignore + return remoteExtensionHostAgentServer.handleUpgrade(req, socket); + }); + server.on('error', async (err) => { + const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer(); + return remoteExtensionHostAgentServer.handleServerError(err); + }); + const nodeListenOptions = ( + parsedArgs['socket-path'] + ? { path: parsedArgs['socket-path'] } + : { host: parsedArgs['host'], port: parsePort(parsedArgs['port']) } + ); + server.listen(nodeListenOptions, async () => { + let output = ``; + + if (typeof nodeListenOptions.port === 'number' && parsedArgs['print-ip-address']) { + const ifaces = os.networkInterfaces(); + Object.keys(ifaces).forEach(function (ifname) { + ifaces[ifname].forEach(function (iface) { + if (!iface.internal && iface.family === 'IPv4') { + output += `IP Address: ${iface.address}\n`; + } + }); + }); + } + + address = server.address(); + if (address === null) { + throw new Error('Unexpected server address'); + } + + // Do not change this line. VS Code looks for this in the output. + output += `Extension host agent listening on ${typeof address === 'string' ? address : address.port}\n`; + console.log(output); + + perf.mark('code/server/started'); + // @ts-ignore + global.vscodeServerListenTime = performance.now(); + + await getRemoteExtensionHostAgentServer(); + }); + + process.on('exit', () => { + server.close(); + if (_remoteExtensionHostAgentServer) { + _remoteExtensionHostAgentServer.dispose(); + } + }); +} + +/** + * @param {string | undefined} strPort + * @returns {number} + */ +function parsePort(strPort) { + try { + if (strPort) { + return parseInt(strPort); + } + } catch (e) { + console.log('Port is not a number, using 8000 instead.'); + } + return 8000; +} + +/** @returns { Promise } */ +function loadCode() { + return new Promise((resolve, reject) => { + const path = require('path'); + + // Set default remote native node modules path, if unset + process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', '..', '..', 'remote', 'node_modules'); + require('../../bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']); + require('../../bootstrap-amd').load('vs/server/remoteExtensionHostAgent', resolve, reject); + }); +} + +start(); diff --git a/src/vs/server/remoteAgentEnvironmentImpl.ts b/src/vs/server/remoteAgentEnvironmentImpl.ts new file mode 100644 index 0000000000000..a930a8b260934 --- /dev/null +++ b/src/vs/server/remoteAgentEnvironmentImpl.ts @@ -0,0 +1,507 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import * as platform from 'vs/base/common/platform'; +import * as performance from 'vs/base/common/performance'; +import { URI } from 'vs/base/common/uri'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IScanExtensionsArguments, IScanSingleExtensionArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; +import * as nls from 'vs/nls'; +import * as fs from 'fs'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import product from 'vs/platform/product/common/product'; +import { ExtensionScanner, ExtensionScannerInput, IExtensionResolver, IExtensionReference } from 'vs/workbench/services/extensions/node/extensionPoints'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; +import { ILogService } from 'vs/platform/log/common/log'; +import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/remoteLanguagePacks'; +import { ContextKeyExpr, ContextKeyDefinedExpr, ContextKeyNotExpr, ContextKeyEqualsExpr, ContextKeyNotEqualsExpr, ContextKeyRegexExpr, IContextKeyExprMapper, ContextKeyExpression, ContextKeyInExpr, ContextKeyGreaterExpr, ContextKeyGreaterEqualsExpr, ContextKeySmallerExpr, ContextKeySmallerEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { listProcesses } from 'vs/base/node/ps'; +import { getMachineInfo, collectWorkspaceStats } from 'vs/platform/diagnostics/node/diagnosticsService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { basename, isAbsolute, join, normalize } from 'vs/base/common/path'; +import { ProcessItem } from 'vs/base/common/processes'; +import { ILog, Translations } from 'vs/workbench/services/extensions/common/extensionPoints'; +import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IBuiltInExtension } from 'vs/base/common/product'; +import { IExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { cwd } from 'vs/base/common/process'; +import { IRemoteTelemetryService } from 'vs/server/remoteTelemetryService'; + +let _SystemExtensionsRoot: string | null = null; +function getSystemExtensionsRoot(): string { + if (!_SystemExtensionsRoot) { + _SystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); + } + return _SystemExtensionsRoot; +} +let _ExtraDevSystemExtensionsRoot: string | null = null; +function getExtraDevSystemExtensionsRoot(): string { + if (!_ExtraDevSystemExtensionsRoot) { + _ExtraDevSystemExtensionsRoot = normalize(join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); + } + return _ExtraDevSystemExtensionsRoot; +} + +export class RemoteAgentEnvironmentChannel implements IServerChannel { + + private static _namePool = 1; + private readonly _logger: ILog; + + private readonly whenExtensionsReady: Promise; + + constructor( + private readonly _connectionToken: string, + private readonly environmentService: IServerEnvironmentService, + extensionManagementCLIService: IExtensionManagementCLIService, + private readonly logService: ILogService, + private readonly telemetryService: IRemoteTelemetryService, + private readonly telemetryAppender: ITelemetryAppender | null + ) { + this._logger = new class implements ILog { + public error(source: string, message: string): void { + logService.error(source, message); + } + public warn(source: string, message: string): void { + logService.warn(source, message); + } + public info(source: string, message: string): void { + logService.info(source, message); + } + }; + + if (environmentService.args['install-builtin-extension']) { + this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], environmentService.args['install-builtin-extension'], !!environmentService.args['do-not-sync'], !!environmentService.args['force']) + .then(null, error => { + logService.error(error); + }); + } else { + this.whenExtensionsReady = Promise.resolve(); + } + + const extensionsToInstall = environmentService.args['install-extension']; + if (extensionsToInstall) { + const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); + this.whenExtensionsReady + .then(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], !!environmentService.args['do-not-sync'], !!environmentService.args['force'])) + .then(null, error => { + logService.error(error); + }); + } + } + + async call(_: any, command: string, arg?: any): Promise { + switch (command) { + case 'disableTelemetry': { + this.telemetryService.permanentlyDisableTelemetry(); + return; + } + + case 'getEnvironmentData': { + const args = arg; + const uriTransformer = createRemoteURITransformer(args.remoteAuthority); + + let environmentData = await this._getEnvironmentData(); + environmentData = transformOutgoingURIs(environmentData, uriTransformer); + + return environmentData; + } + + case 'whenExtensionsReady': { + await this.whenExtensionsReady; + return; + } + + case 'scanExtensions': { + await this.whenExtensionsReady; + const args = arg; + const language = args.language; + this.logService.trace(`Scanning extensions using UI language: ${language}`); + const uriTransformer = createRemoteURITransformer(args.remoteAuthority); + + const extensionDevelopmentLocations = args.extensionDevelopmentPath && args.extensionDevelopmentPath.map(url => URI.revive(uriTransformer.transformIncoming(url))); + const extensionDevelopmentPath = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; + + let extensions = await this._scanExtensions(language, extensionDevelopmentPath); + extensions = transformOutgoingURIs(extensions, uriTransformer); + + this.logService.trace('Scanned Extensions', extensions); + RemoteAgentEnvironmentChannel._massageWhenConditions(extensions); + + return extensions; + } + + case 'scanSingleExtension': { + await this.whenExtensionsReady; + const args = arg; + const language = args.language; + const isBuiltin = args.isBuiltin; + const uriTransformer = createRemoteURITransformer(args.remoteAuthority); + const extensionLocation = URI.revive(uriTransformer.transformIncoming(args.extensionLocation)); + const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null; + + if (!extensionPath) { + return null; + } + + const translations = await this._getTranslations(language); + let extension = await this._scanSingleExtension(extensionPath, isBuiltin, language, translations); + + if (!extension) { + return null; + } + + extension = transformOutgoingURIs(extension, uriTransformer); + + RemoteAgentEnvironmentChannel._massageWhenConditions([extension]); + + return extension; + } + + case 'getDiagnosticInfo': { + const options = arg; + const diagnosticInfo: IDiagnosticInfo = { + machineInfo: getMachineInfo() + }; + + const processesPromise: Promise = options.includeProcesses ? listProcesses(process.pid) : Promise.resolve(); + + let workspaceMetadataPromises: Promise[] = []; + const workspaceMetadata: { [key: string]: any } = {}; + if (options.folders) { + // only incoming paths are transformed, so remote authority is unneeded. + const uriTransformer = createRemoteURITransformer(''); + const folderPaths = options.folders + .map(folder => URI.revive(uriTransformer.transformIncoming(folder))) + .filter(uri => uri.scheme === 'file'); + + workspaceMetadataPromises = folderPaths.map(folder => { + return collectWorkspaceStats(folder.fsPath, ['node_modules', '.git']) + .then(stats => { + workspaceMetadata[basename(folder.fsPath)] = stats; + }); + }); + } + + return Promise.all([processesPromise, ...workspaceMetadataPromises]).then(([processes, _]) => { + diagnosticInfo.processes = processes || undefined; + diagnosticInfo.workspaceMetadata = options.folders ? workspaceMetadata : undefined; + return diagnosticInfo; + }); + } + + case 'logTelemetry': { + const { eventName, data } = arg; + // Logging is done directly to the appender instead of through the telemetry service + // as the data sent from the client has already had common properties added to it and + // has already been sent to the telemetry output channel + if (this.telemetryAppender) { + return this.telemetryAppender.log(eventName, data); + } + + return Promise.resolve(); + } + + case 'flushTelemetry': { + if (this.telemetryAppender) { + return this.telemetryAppender.flush(); + } + + return Promise.resolve(); + } + } + + throw new Error(`IPC Command ${command} not found`); + } + + listen(_: any, event: string, arg: any): Event { + throw new Error('Not supported'); + } + + private static _massageWhenConditions(extensions: IExtensionDescription[]): void { + // Massage "when" conditions which mention `resourceScheme` + + interface WhenUser { when?: string; } + + interface LocWhenUser { [loc: string]: WhenUser[]; } + + const _mapResourceSchemeValue = (value: string, isRegex: boolean): string => { + // console.log(`_mapResourceSchemeValue: ${value}, ${isRegex}`); + return value.replace(/file/g, 'vscode-remote'); + }; + + const _mapResourceRegExpValue = (value: RegExp): RegExp => { + let flags = ''; + flags += value.global ? 'g' : ''; + flags += value.ignoreCase ? 'i' : ''; + flags += value.multiline ? 'm' : ''; + return new RegExp(_mapResourceSchemeValue(value.source, true), flags); + }; + + const _exprKeyMapper = new class implements IContextKeyExprMapper { + mapDefined(key: string): ContextKeyExpression { + return ContextKeyDefinedExpr.create(key); + } + mapNot(key: string): ContextKeyExpression { + return ContextKeyNotExpr.create(key); + } + mapEquals(key: string, value: any): ContextKeyExpression { + if (key === 'resourceScheme' && typeof value === 'string') { + return ContextKeyEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); + } else { + return ContextKeyEqualsExpr.create(key, value); + } + } + mapNotEquals(key: string, value: any): ContextKeyExpression { + if (key === 'resourceScheme' && typeof value === 'string') { + return ContextKeyNotEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); + } else { + return ContextKeyNotEqualsExpr.create(key, value); + } + } + mapGreater(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterExpr.create(key, value); + } + mapGreaterEquals(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterEqualsExpr.create(key, value); + } + mapSmaller(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerExpr.create(key, value); + } + mapSmallerEquals(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerEqualsExpr.create(key, value); + } + mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr { + if (key === 'resourceScheme' && regexp) { + return ContextKeyRegexExpr.create(key, _mapResourceRegExpValue(regexp)); + } else { + return ContextKeyRegexExpr.create(key, regexp); + } + } + mapIn(key: string, valueKey: string): ContextKeyInExpr { + return ContextKeyInExpr.create(key, valueKey); + } + }; + + const _massageWhenUser = (element: WhenUser) => { + if (!element || !element.when || !/resourceScheme/.test(element.when)) { + return; + } + + const expr = ContextKeyExpr.deserialize(element.when); + if (!expr) { + return; + } + + const massaged = expr.map(_exprKeyMapper); + element.when = massaged.serialize(); + }; + + const _massageWhenUserArr = (elements: WhenUser[] | WhenUser) => { + if (Array.isArray(elements)) { + for (let element of elements) { + _massageWhenUser(element); + } + } else { + _massageWhenUser(elements); + } + }; + + const _massageLocWhenUser = (target: LocWhenUser) => { + for (let loc in target) { + _massageWhenUserArr(target[loc]); + } + }; + + extensions.forEach((extension) => { + if (extension.contributes) { + if (extension.contributes.menus) { + _massageLocWhenUser(extension.contributes.menus); + } + if (extension.contributes.keybindings) { + _massageWhenUserArr(extension.contributes.keybindings); + } + if (extension.contributes.views) { + _massageLocWhenUser(extension.contributes.views); + } + } + }); + } + + private async _getEnvironmentData(): Promise { + return { + pid: process.pid, + connectionToken: this._connectionToken, + appRoot: URI.file(this.environmentService.appRoot), + settingsPath: this.environmentService.machineSettingsResource, + logsPath: URI.file(this.environmentService.logsPath), + extensionsPath: URI.file(this.environmentService.extensionsPath!), + extensionHostLogsPath: URI.file(join(this.environmentService.logsPath, `exthost${RemoteAgentEnvironmentChannel._namePool++}`)), + globalStorageHome: this.environmentService.globalStorageHome, + workspaceStorageHome: this.environmentService.workspaceStorageHome, + userHome: this.environmentService.userHome, + os: platform.OS, + arch: process.arch, + marks: performance.getMarks(), + useHostProxy: (this.environmentService.args['use-host-proxy'] !== undefined) + }; + } + + private async _getTranslations(language: string): Promise { + const config = await getNLSConfiguration(language, this.environmentService.userDataPath); + if (InternalNLSConfiguration.is(config)) { + try { + const content = await fs.promises.readFile(config._translationsConfigFile, 'utf8'); + return JSON.parse(content); + } catch (err) { + return Object.create(null); + } + } else { + return Object.create(null); + } + } + + private async _scanExtensions(language: string, extensionDevelopmentPath?: string[]): Promise { + // Ensure that the language packs are available + const translations = await this._getTranslations(language); + + const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ + this._scanBuiltinExtensions(language, translations), + this._scanInstalledExtensions(language, translations), + this._scanDevelopedExtensions(language, translations, extensionDevelopmentPath) + ]); + + let result = new Map(); + + builtinExtensions.forEach((builtinExtension) => { + if (!builtinExtension) { + return; + } + result.set(ExtensionIdentifier.toKey(builtinExtension.identifier), builtinExtension); + }); + + installedExtensions.forEach((installedExtension) => { + if (!installedExtension) { + return; + } + if (result.has(ExtensionIdentifier.toKey(installedExtension.identifier))) { + console.warn(nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result.get(ExtensionIdentifier.toKey(installedExtension.identifier))!.extensionLocation.fsPath, installedExtension.extensionLocation.fsPath)); + } + result.set(ExtensionIdentifier.toKey(installedExtension.identifier), installedExtension); + }); + + developedExtensions.forEach((developedExtension) => { + if (!developedExtension) { + return; + } + result.set(ExtensionIdentifier.toKey(developedExtension.identifier), developedExtension); + }); + + const r: IExtensionDescription[] = []; + result.forEach((v) => r.push(v)); + return r; + } + + private _scanDevelopedExtensions(language: string, translations: Translations, extensionDevelopmentPaths?: string[]): Promise { + + if (extensionDevelopmentPaths) { + + const extDescsP = extensionDevelopmentPaths.map(extDevPath => { + return ExtensionScanner.scanOneOrMultipleExtensions( + new ExtensionScannerInput( + product.version, + product.date, + product.commit, + language, + true, // dev mode + extDevPath, + false, // isBuiltin + true, // isUnderDevelopment + translations // translations + ), this._logger + ); + }); + + return Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { + let extDesc: IExtensionDescription[] = []; + for (let eds of extDescArrays) { + extDesc = extDesc.concat(eds); + } + return extDesc; + }); + } + return Promise.resolve([]); + } + + private _scanBuiltinExtensions(language: string, translations: Translations): Promise { + const version = product.version; + const commit = product.commit; + const date = product.date; + const devMode = !!process.env['VSCODE_DEV']; + + const input = new ExtensionScannerInput(version, date, commit, language, devMode, getSystemExtensionsRoot(), true, false, translations); + const builtinExtensions = ExtensionScanner.scanExtensions(input, this._logger); + let finalBuiltinExtensions: Promise = builtinExtensions; + + if (devMode) { + + class ExtraBuiltInExtensionResolver implements IExtensionResolver { + constructor(private builtInExtensions: IBuiltInExtension[]) { } + resolveExtensions(): Promise { + return Promise.resolve(this.builtInExtensions.map((ext) => { + return { name: ext.name, path: join(getExtraDevSystemExtensionsRoot(), ext.name) }; + })); + } + } + + const builtInExtensions = Promise.resolve(product.builtInExtensions || []); + + const input = new ExtensionScannerInput(version, date, commit, language, devMode, getExtraDevSystemExtensionsRoot(), true, false, {}); + const extraBuiltinExtensions = builtInExtensions + .then((builtInExtensions) => new ExtraBuiltInExtensionResolver(builtInExtensions)) + .then(resolver => ExtensionScanner.scanExtensions(input, this._logger, resolver)); + + finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions); + } + + return finalBuiltinExtensions; + } + + private _scanInstalledExtensions(language: string, translations: Translations): Promise { + const devMode = !!process.env['VSCODE_DEV']; + const input = new ExtensionScannerInput( + product.version, + product.date, + product.commit, + language, + devMode, + this.environmentService.extensionsPath!, + false, // isBuiltin + false, // isUnderDevelopment + translations + ); + + return ExtensionScanner.scanExtensions(input, this._logger); + } + + private _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string, translations: Translations): Promise { + const devMode = !!process.env['VSCODE_DEV']; + const input = new ExtensionScannerInput( + product.version, + product.date, + product.commit, + language, + devMode, + extensionPath, + isBuiltin, + false, // isUnderDevelopment + translations + ); + return ExtensionScanner.scanSingleExtension(input, this._logger); + } +} diff --git a/src/vs/server/remoteAgentFileSystemImpl.ts b/src/vs/server/remoteAgentFileSystemImpl.ts new file mode 100644 index 0000000000000..28ebc621b41b4 --- /dev/null +++ b/src/vs/server/remoteAgentFileSystemImpl.ts @@ -0,0 +1,305 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { FileDeleteOptions, FileOverwriteOptions, FileType, IFileChange, IStat, IWatchOptions, FileOpenOptions, FileWriteOptions, FileReadStreamOptions } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { DiskFileSystemProvider, IWatcherOptions } from 'vs/platform/files/node/diskFileSystemProvider'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { posix, delimiter } from 'vs/base/common/path'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { listenStream, ReadableStreamEventPayload } from 'vs/base/common/stream'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +class SessionFileWatcher extends Disposable { + private readonly watcherRequests = new Map(); + private readonly fileWatcher = this._register(new DiskFileSystemProvider(this.logService, { watcher: this.getWatcherOptions() })); + + constructor( + private readonly logService: ILogService, + private readonly environmentService: IServerEnvironmentService, + private readonly uriTransformer: IURITransformer, + emitter: Emitter + ) { + super(); + + this.registerListeners(emitter); + } + + private registerListeners(emitter: Emitter): void { + const localChangeEmitter = this._register(new Emitter()); + + this._register(localChangeEmitter.event((events) => { + emitter.fire( + events.map(e => ({ + resource: this.uriTransformer.transformOutgoingURI(e.resource), + type: e.type + })) + ); + })); + + this._register(this.fileWatcher.onDidChangeFile(events => localChangeEmitter.fire(events))); + this._register(this.fileWatcher.onDidErrorOccur(error => emitter.fire(error))); + } + + private getWatcherOptions(): IWatcherOptions | undefined { + const fileWatcherPolling = this.environmentService.args['fileWatcherPolling']; + if (fileWatcherPolling) { + const segments = fileWatcherPolling.split(delimiter); + const pollingInterval = Number(segments[0]); + if (pollingInterval > 0) { + const usePolling = segments.length > 1 ? segments.slice(1) : true; + return { usePolling, pollingInterval }; + } + } + + return undefined; + } + + watch(req: number, _resource: UriComponents, opts: IWatchOptions): IDisposable { + const resource = URI.revive(this.uriTransformer.transformIncoming(_resource)); + + if (this.environmentService.extensionsPath) { + // when opening the $HOME folder, we end up watching the extension folder + // so simply exclude watching the extensions folder + + opts.excludes = [...(opts.excludes || []), posix.join(this.environmentService.extensionsPath, '**')]; + } + + this.watcherRequests.set(req, this.fileWatcher.watch(resource, opts)); + + return toDisposable(() => { + dispose(this.watcherRequests.get(req)); + this.watcherRequests.delete(req); + }); + } + + override dispose(): void { + super.dispose(); + + this.watcherRequests.forEach(disposable => dispose(disposable)); + this.watcherRequests.clear(); + } +} + +export class RemoteAgentFileSystemChannel extends Disposable implements IServerChannel { + + private readonly BUFFER_SIZE = 256 * 1024; // slightly larger to reduce remote-communication overhead + + private readonly uriTransformerCache = new Map(); + private readonly fileWatchers = new Map(); + private readonly fsProvider = this._register(new DiskFileSystemProvider(this.logService, { bufferSize: this.BUFFER_SIZE })); + private readonly watchRequests = new Map(); + + constructor( + private readonly logService: ILogService, + private readonly environmentService: IServerEnvironmentService + ) { + super(); + } + + call(ctx: RemoteAgentConnectionContext, command: string, arg?: any): Promise { + const uriTransformer = this.getUriTransformer(ctx.remoteAuthority); + + switch (command) { + case 'stat': return this.stat(uriTransformer, arg[0]); + case 'readdir': return this.readdir(uriTransformer, arg[0]); + case 'open': return this.open(uriTransformer, arg[0], arg[1]); + case 'close': return this.close(arg[0]); + case 'read': return this.read(arg[0], arg[1], arg[2]); + case 'readFile': return this.readFile(uriTransformer, arg[0]); + case 'write': return this.write(arg[0], arg[1], arg[2], arg[3], arg[4]); + case 'writeFile': return this.writeFile(uriTransformer, arg[0], arg[1], arg[2]); + case 'rename': return this.rename(uriTransformer, arg[0], arg[1], arg[2]); + case 'copy': return this.copy(uriTransformer, arg[0], arg[1], arg[2]); + case 'mkdir': return this.mkdir(uriTransformer, arg[0]); + case 'delete': return this.delete(uriTransformer, arg[0], arg[1]); + case 'watch': return Promise.resolve(this.watch(arg[0], arg[1], arg[2], arg[3])); + case 'unwatch': return Promise.resolve(this.unwatch(arg[0], arg[1])); + } + + throw new Error(`IPC Command ${command} not found`); + } + + listen(ctx: RemoteAgentConnectionContext, event: string, arg: any): Event { + const uriTransformer = this.getUriTransformer(ctx.remoteAuthority); + + switch (event) { + case 'filechange': return this.onFileChange(uriTransformer, arg[0]); + case 'readFileStream': return this.onReadFileStream(uriTransformer, arg[0], arg[1]); + } + + throw new Error(`Unknown event ${event}`); + } + + private onFileChange(uriTransformer: IURITransformer, session: string): Event { + const emitter = new Emitter({ + onFirstListenerAdd: () => { + this.fileWatchers.set(session, new SessionFileWatcher(this.logService, this.environmentService, uriTransformer, emitter)); + }, + onLastListenerRemove: () => { + dispose(this.fileWatchers.get(session)); + this.fileWatchers.delete(session); + } + }); + + return emitter.event; + } + + private onReadFileStream(uriTransformer: IURITransformer, _resource: URI, opts: FileReadStreamOptions): Event> { + const resource = this.transformIncoming(uriTransformer, _resource, true); + const cancellableSource = new CancellationTokenSource(); + + const emitter = new Emitter>({ + onLastListenerRemove: () => { + + // Ensure to cancel the read operation when there is no more + // listener on the other side to prevent unneeded work. + cancellableSource.cancel(); + } + }); + + const fileStream = this.fsProvider.readFileStream(resource, opts, cancellableSource.token); + listenStream(fileStream, { + onData: chunk => emitter.fire(VSBuffer.wrap(chunk)), + onError: error => emitter.fire(error), + onEnd: () => { + emitter.fire('end'); + + // Cleanup + emitter.dispose(); + cancellableSource.dispose(); + } + }); + + return emitter.event; + } + + private getUriTransformer(remoteAuthority: string): IURITransformer { + let transformer = this.uriTransformerCache.get(remoteAuthority); + if (!transformer) { + transformer = createRemoteURITransformer(remoteAuthority); + this.uriTransformerCache.set(remoteAuthority, transformer); + } + + return transformer; + } + + private stat(uriTransformer: IURITransformer, _resource: UriComponents): Promise { + const resource = this.transformIncoming(uriTransformer, _resource, true); + + return this.fsProvider.stat(resource); + } + + private readdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<[string, FileType][]> { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.fsProvider.readdir(resource); + } + + private open(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileOpenOptions): Promise { + const resource = this.transformIncoming(uriTransformer, _resource, true); + + return this.fsProvider.open(resource, opts); + } + + private close(_fd: number): Promise { + return this.fsProvider.close(_fd); + } + + private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> { + const buffer = VSBuffer.alloc(length); + const bufferOffset = 0; // offset is 0 because we create a buffer to read into for each call + const bytesRead = await this.fsProvider.read(fd, pos, buffer.buffer, bufferOffset, length); + + return [buffer, bytesRead]; + } + + private async readFile(uriTransformer: IURITransformer, _resource: UriComponents): Promise { + const resource = this.transformIncoming(uriTransformer, _resource, true); + const buff = await this.fsProvider.readFile(resource); + + return VSBuffer.wrap(buff); + } + + private write(fd: number, pos: number, data: VSBuffer, offset: number, length: number): Promise { + return this.fsProvider.write(fd, pos, data.buffer, offset, length); + } + + private writeFile(uriTransformer: IURITransformer, _resource: UriComponents, content: VSBuffer, opts: FileWriteOptions): Promise { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.fsProvider.writeFile(resource, content.buffer, opts); + } + + private rename(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: FileOverwriteOptions): Promise { + const source = URI.revive(uriTransformer.transformIncoming(_source)); + const target = URI.revive(uriTransformer.transformIncoming(_target)); + + return this.fsProvider.rename(source, target, opts); + } + + private copy(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: FileOverwriteOptions): Promise { + const source = this.transformIncoming(uriTransformer, _source); + const target = this.transformIncoming(uriTransformer, _target); + + return this.fsProvider.copy(source, target, opts); + } + + private mkdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.fsProvider.mkdir(resource); + } + + private delete(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileDeleteOptions): Promise { + const resource = this.transformIncoming(uriTransformer, _resource); + + return this.fsProvider.delete(resource, opts); + } + + private transformIncoming(uriTransformer: IURITransformer, _resource: UriComponents, supportVSCodeResource = false): URI { + if (supportVSCodeResource && _resource.path === '/vscode-resource' && _resource.query) { + const requestResourcePath = JSON.parse(_resource.query).requestResourcePath; + return URI.from({ scheme: 'file', path: requestResourcePath }); + } + + return URI.revive(uriTransformer.transformIncoming(_resource)); + } + + private watch(session: string, req: number, _resource: UriComponents, opts: IWatchOptions): void { + const id = session + req; + const watcher = this.fileWatchers.get(session); + if (watcher) { + const disposable = watcher.watch(req, _resource, opts); + this.watchRequests.set(id, disposable); + } + } + + private unwatch(session: string, req: number): void { + const id = session + req; + const disposable = this.watchRequests.get(id); + if (disposable) { + dispose(disposable); + this.watchRequests.delete(id); + } + } + + override dispose(): void { + super.dispose(); + + this.watchRequests.forEach(disposable => dispose(disposable)); + this.watchRequests.clear(); + + this.fileWatchers.forEach(disposable => dispose(disposable)); + this.fileWatchers.clear(); + } +} diff --git a/src/vs/server/remoteCli.ts b/src/vs/server/remoteCli.ts new file mode 100644 index 0000000000000..e88542faeac13 --- /dev/null +++ b/src/vs/server/remoteCli.ts @@ -0,0 +1,400 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as _fs from 'fs'; +import * as _url from 'url'; +import * as _cp from 'child_process'; +import * as _http from 'http'; +import * as _os from 'os'; +import { cwd } from 'vs/base/common/process'; +import { dirname, extname, resolve, join } from 'vs/base/common/path'; +import { parseArgs, buildHelpMessage, buildVersionMessage, OPTIONS, OptionDescriptions } from 'vs/platform/environment/node/argv'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; +import { PipeCommand } from 'vs/workbench/api/node/extHostCLIServer'; +import { hasStdinWithoutTty, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; + +interface ProductDescription { + productName: string; + version: string; + commit: string; + executableName: string; +} + +interface RemoteParsedArgs extends NativeParsedArgs { 'gitCredential'?: string; 'openExternal'?: boolean; } + + +const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => { + switch (optionId) { + case 'user-data-dir': + case 'extensions-dir': + case 'export-default-configuration': + case 'install-source': + case 'driver': + case 'extensions-download-dir': + case 'builtin-extensions-dir': + case 'telemetry': + return false; + default: + return true; + } +}; + +const isSupportedForPipe = (optionId: keyof RemoteParsedArgs) => { + switch (optionId) { + case 'version': + case 'help': + case 'folder-uri': + case 'file-uri': + case 'add': + case 'diff': + case 'wait': + case 'goto': + case 'reuse-window': + case 'new-window': + case 'status': + case 'install-extension': + case 'uninstall-extension': + case 'list-extensions': + case 'force': + case 'show-versions': + case 'category': + return true; + default: + return false; + } +}; + +const cliPipe = process.env['VSCODE_IPC_HOOK_CLI'] as string; +const cliCommand = process.env['VSCODE_CLIENT_COMMAND'] as string; +const cliCommandCwd = process.env['VSCODE_CLIENT_COMMAND_CWD'] as string; +const remoteAuthority = process.env['VSCODE_CLI_AUTHORITY'] as string; +const cliStdInFilePath = process.env['VSCODE_STDIN_FILE_PATH'] as string; + + +export function main(desc: ProductDescription, args: string[]): void { + if (!cliPipe && !cliCommand) { + console.log('Command is only available in WSL or inside a Visual Studio Code terminal.'); + return; + } + + // take the local options and remove the ones that don't apply + const options: OptionDescriptions = { ...OPTIONS }; + const isSupported = cliCommand ? isSupportedForCmd : isSupportedForPipe; + for (const optionId in OPTIONS) { + const optId = optionId; + if (!isSupported(optId)) { + delete options[optId]; + } + } + + if (cliPipe) { + options['openExternal'] = { type: 'boolean' }; + } + + const errorReporter = { + onMultipleValues: (id: string, usedValue: string) => { + console.error(`Option ${id} can only be defined once. Using value ${usedValue}.`); + }, + + onUnknownOption: (id: string) => { + console.error(`Ignoring option ${id}: not supported for ${desc.executableName}.`); + } + }; + + const parsedArgs = parseArgs(args, options, errorReporter); + const mapFileUri = remoteAuthority ? mapFileToRemoteUri : (uri: string) => uri; + + if (parsedArgs.help) { + console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options, true)); + return; + } + if (parsedArgs.version) { + console.log(buildVersionMessage(desc.version, desc.commit)); + return; + } + if (cliPipe) { + if (parsedArgs['openExternal']) { + openInBrowser(parsedArgs['_']); + return; + } + } + + + let folderURIs = (parsedArgs['folder-uri'] || []).map(mapFileUri); + parsedArgs['folder-uri'] = folderURIs; + + let fileURIs = (parsedArgs['file-uri'] || []).map(mapFileUri); + parsedArgs['file-uri'] = fileURIs; + + let inputPaths = parsedArgs['_']; + let hasReadStdinArg = false; + for (let input of inputPaths) { + if (input === '-') { + hasReadStdinArg = true; + } else { + translatePath(input, mapFileUri, folderURIs, fileURIs); + } + } + + parsedArgs['_'] = []; + + if (hasReadStdinArg && fileURIs.length === 0 && folderURIs.length === 0 && hasStdinWithoutTty()) { + try { + let stdinFilePath = cliStdInFilePath; + if (!stdinFilePath) { + stdinFilePath = getStdinFilePath(); + readFromStdin(stdinFilePath, !!parsedArgs.verbose); // throws error if file can not be written + } + + // Make sure to open tmp file + translatePath(stdinFilePath, mapFileUri, folderURIs, fileURIs); + + // Enable --wait to get all data and ignore adding this to history + parsedArgs.wait = true; + parsedArgs['skip-add-to-recently-opened'] = true; + + console.log(`Reading from stdin via: ${stdinFilePath}`); + } catch (e) { + console.log(`Failed to create file to read via stdin: ${e.toString()}`); + } + + } + + if (parsedArgs.extensionDevelopmentPath) { + parsedArgs.extensionDevelopmentPath = parsedArgs.extensionDevelopmentPath.map(p => mapFileUri(pathToURI(p).href)); + } + + if (parsedArgs.extensionTestsPath) { + parsedArgs.extensionTestsPath = mapFileUri(pathToURI(parsedArgs['extensionTestsPath']).href); + } + + const crashReporterDirectory = parsedArgs['crash-reporter-directory']; + if (crashReporterDirectory !== undefined && !crashReporterDirectory.match(/^([a-zA-Z]:[\\\/])/)) { + console.log(`The crash reporter directory '${crashReporterDirectory}' must be an absolute Windows path (e.g. c:/crashes)`); + return; + } + + if (remoteAuthority) { + parsedArgs['remote'] = remoteAuthority; + } + + if (cliCommand) { + if (parsedArgs['install-extension'] !== undefined || parsedArgs['uninstall-extension'] !== undefined || parsedArgs['list-extensions']) { + const cmdLine: string[] = []; + parsedArgs['install-extension']?.forEach(id => cmdLine.push('--install-extension', id)); + parsedArgs['uninstall-extension']?.forEach(id => cmdLine.push('--uninstall-extension', id)); + ['list-extensions', 'force', 'show-versions', 'category'].forEach(opt => { + const value = parsedArgs[opt]; + if (value !== undefined) { + cmdLine.push(`--${opt}=${value}`); + } + }); + const cp = _cp.fork(join(__dirname, 'main.js'), cmdLine, { stdio: 'inherit' }); + cp.on('error', err => console.log(err)); + return; + } + + + let newCommandline: string[] = []; + for (let key in parsedArgs) { + let val = parsedArgs[key as keyof typeof parsedArgs]; + if (typeof val === 'boolean') { + if (val) { + newCommandline.push('--' + key); + } + } else if (Array.isArray(val)) { + for (let entry of val) { + newCommandline.push(`--${key}=${entry.toString()}`); + } + } else if (val) { + newCommandline.push(`--${key}=${val.toString()}`); + } + } + + const ext = extname(cliCommand); + if (ext === '.bat' || ext === '.cmd') { + const processCwd = cliCommandCwd || cwd(); + if (parsedArgs['verbose']) { + console.log(`Invoking: cmd.exe /C ${cliCommand} ${newCommandline.join(' ')} in ${processCwd}`); + } + _cp.spawn('cmd.exe', ['/C', cliCommand, ...newCommandline], { + stdio: 'inherit', + cwd: processCwd + }); + } else { + const cliCwd = dirname(cliCommand); + const env = { ...process.env, ELECTRON_RUN_AS_NODE: '1' }; + newCommandline.unshift('resources/app/out/cli.js'); + if (parsedArgs['verbose']) { + console.log(`Invoking: ${cliCommand} ${newCommandline.join(' ')} in ${cliCwd}`); + } + _cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: ['inherit'] }); + } + } else { + if (args.length === 0) { + console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options, true)); + return; + } + if (parsedArgs.status) { + sendToPipe({ + type: 'status' + }).then((res: string) => { + console.log(res); + }); + return; + } + + if (parsedArgs['install-extension'] !== undefined || parsedArgs['uninstall-extension'] !== undefined || parsedArgs['list-extensions']) { + sendToPipe({ + type: 'extensionManagement', + list: parsedArgs['list-extensions'] ? { showVersions: parsedArgs['show-versions'], category: parsedArgs['category'] } : undefined, + install: asExtensionIdOrVSIX(parsedArgs['install-extension']), + uninstall: asExtensionIdOrVSIX(parsedArgs['uninstall-extension']), + force: parsedArgs['force'] + }).then((res: string) => { + console.log(res); + }); + return; + } + + if (!fileURIs.length && !folderURIs.length) { + console.log('At least one file or folder must be provided.'); + return; + } + + let waitMarkerFilePath: string | undefined = undefined; + if (parsedArgs['wait']) { + if (!fileURIs.length) { + console.log('At least one file must be provided to wait for.'); + return; + } + waitMarkerFilePath = createWaitMarkerFile(parsedArgs.verbose); + } + + sendToPipe({ + type: 'open', + fileURIs, + folderURIs, + diffMode: parsedArgs.diff, + addMode: parsedArgs.add, + gotoLineMode: parsedArgs.goto, + forceReuseWindow: parsedArgs['reuse-window'], + forceNewWindow: parsedArgs['new-window'], + waitMarkerFilePath + }); + + if (waitMarkerFilePath) { + waitForFileDeleted(waitMarkerFilePath); + } + } +} + +async function waitForFileDeleted(path: string) { + while (_fs.existsSync(path)) { + await new Promise(res => setTimeout(res, 1000)); + } +} + +function openInBrowser(args: string[]) { + let uris: string[] = []; + for (let location of args) { + try { + if (/^(http|https|file):\/\//.test(location)) { + uris.push(_url.parse(location).href); + } else { + uris.push(pathToURI(location).href); + } + } catch (e) { + console.log(`Invalid url: ${location}`); + } + } + if (uris.length) { + sendToPipe({ + type: 'openExternal', + uris + }); + } +} + +function sendToPipe(args: PipeCommand): Promise { + return new Promise(resolve => { + const message = JSON.stringify(args); + if (!cliPipe) { + console.log('Message ' + message); + resolve(''); + return; + } + + const opts: _http.RequestOptions = { + socketPath: cliPipe, + path: '/', + method: 'POST' + }; + + const req = _http.request(opts, res => { + const chunks: string[] = []; + res.setEncoding('utf8'); + res.on('data', chunk => { + chunks.push(chunk); + }); + res.on('error', () => fatal('Error in response')); + res.on('end', () => { + resolve(chunks.join('')); + }); + }); + + req.on('error', () => fatal('Error in request')); + req.write(message); + req.end(); + }); +} + +function asExtensionIdOrVSIX(inputs: string[] | undefined) { + return inputs?.map(input => /\.vsix$/i.test(input) ? pathToURI(input).href : input); +} + +function fatal(err: any): void { + console.error('Unable to connect to VS Code server.'); + console.error(err); + process.exit(1); +} + +const preferredCwd = process.env.PWD || cwd(); // prefer process.env.PWD as it does not follow symlinks + +function pathToURI(input: string): _url.URL { + input = input.trim(); + input = resolve(preferredCwd, input); + + return _url.pathToFileURL(input); +} + +function translatePath(input: string, mapFileUri: (input: string) => string, folderURIS: string[], fileURIS: string[]) { + let url = pathToURI(input); + let mappedUri = mapFileUri(url.href); + try { + let stat = _fs.lstatSync(_fs.realpathSync(input)); + + if (stat.isFile()) { + fileURIS.push(mappedUri); + } else { + folderURIS.push(mappedUri); + } + } catch (e) { + if (e.code === 'ENOENT') { + fileURIS.push(mappedUri); + } else { + console.log(`Problem accessing file ${input}. Ignoring file`, e); + } + } +} + +function mapFileToRemoteUri(uri: string): string { + return uri.replace(/^file:\/\//, 'vscode-remote://' + remoteAuthority); +} + +let [, , productName, version, commit, executableName, ...remainingArgs] = process.argv; +main({ productName, version, commit, executableName }, remainingArgs); + diff --git a/src/vs/server/remoteExtensionHostAgent.ts b/src/vs/server/remoteExtensionHostAgent.ts new file mode 100644 index 0000000000000..ab226cd31f45d --- /dev/null +++ b/src/vs/server/remoteExtensionHostAgent.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as fs from 'fs'; +import * as net from 'net'; +import { FileAccess } from 'vs/base/common/network'; +import { run as runCli } from 'vs/server/remoteExtensionHostAgentCli'; +import { createServer as doCreateServer, IServerAPI } from 'vs/server/remoteExtensionHostAgentServer'; +import { parseArgs, ErrorReporter } from 'vs/platform/environment/node/argv'; +import { join, dirname } from 'vs/base/common/path'; +import { performance } from 'perf_hooks'; +import { serverOptions } from 'vs/server/serverEnvironmentService'; +import * as perf from 'vs/base/common/performance'; + +perf.mark('code/server/codeLoaded'); +(global).vscodeServerCodeLoadedTime = performance.now(); + +const errorReporter: ErrorReporter = { + onMultipleValues: (id: string, usedValue: string) => { + console.error(`Option ${id} can only be defined once. Using value ${usedValue}.`); + }, + + onUnknownOption: (id: string) => { + console.error(`Ignoring option ${id}: not supported for server.`); + } +}; + +const args = parseArgs(process.argv.slice(2), serverOptions, errorReporter); + +const REMOTE_DATA_FOLDER = process.env['VSCODE_AGENT_FOLDER'] || join(os.homedir(), '.vscode-remote'); +const USER_DATA_PATH = join(REMOTE_DATA_FOLDER, 'data'); +const APP_SETTINGS_HOME = join(USER_DATA_PATH, 'User'); +const GLOBAL_STORAGE_HOME = join(APP_SETTINGS_HOME, 'globalStorage'); +const MACHINE_SETTINGS_HOME = join(USER_DATA_PATH, 'Machine'); +args['user-data-dir'] = USER_DATA_PATH; +const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath); +const BUILTIN_EXTENSIONS_FOLDER_PATH = join(APP_ROOT, 'extensions'); +args['builtin-extensions-dir'] = BUILTIN_EXTENSIONS_FOLDER_PATH; +args['extensions-dir'] = args['extensions-dir'] || join(REMOTE_DATA_FOLDER, 'extensions'); + +[REMOTE_DATA_FOLDER, args['extensions-dir'], USER_DATA_PATH, APP_SETTINGS_HOME, MACHINE_SETTINGS_HOME, GLOBAL_STORAGE_HOME].forEach(f => { + try { + if (!fs.existsSync(f)) { + fs.mkdirSync(f); + } + } catch (err) { console.error(err); } +}); + +/** + * invoked by vs/server/main.js + */ +export function spawnCli() { + runCli(args, REMOTE_DATA_FOLDER); +} + +/** + * invoked by vs/server/main.js + */ +export function createServer(address: string | net.AddressInfo | null): Promise { + return doCreateServer(address, args, REMOTE_DATA_FOLDER); +} diff --git a/src/vs/server/remoteExtensionHostAgentCli.ts b/src/vs/server/remoteExtensionHostAgentCli.ts new file mode 100644 index 0000000000000..28bb24ab0e278 --- /dev/null +++ b/src/vs/server/remoteExtensionHostAgentCli.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { getLogLevel, ILogService, LogService } from 'vs/platform/log/common/log'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { RequestService } from 'vs/platform/request/node/requestService'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import product from 'vs/platform/product/common/product'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/serverEnvironmentService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { getErrorMessage } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; +import { isAbsolute, join } from 'vs/base/common/path'; +import { cwd } from 'vs/base/common/process'; +import { DownloadService } from 'vs/platform/download/common/downloadService'; +import { IDownloadService } from 'vs/platform/download/common/download'; + +class CliMain extends Disposable { + + constructor(private readonly args: ServerParsedArgs, private readonly remoteDataFolder: string) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + // Dispose on exit + process.once('exit', () => this.dispose()); + } + + async run(): Promise { + const instantiationService = await this.initServices(); + await instantiationService.invokeFunction(async accessor => { + const logService = accessor.get(ILogService); + const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService); + try { + await this.doRun(extensionManagementCLIService); + } catch (error) { + logService.error(error); + console.error(getErrorMessage(error)); + throw error; + } + }); + } + + private async initServices(): Promise { + const services = new ServiceCollection(); + + const productService = { _serviceBrand: undefined, ...product }; + services.set(IProductService, productService); + + const environmentService = new ServerEnvironmentService(this.args, productService); + services.set(IServerEnvironmentService, environmentService); + const logService: ILogService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, getLogLevel(environmentService))); + services.set(ILogService, logService); + logService.trace(`Remote configuration data at ${this.remoteDataFolder}`); + logService.trace('process arguments:', this.args); + + + // Files + const fileService = this._register(new FileService(logService)); + services.set(IFileService, fileService); + fileService.registerProvider(Schemas.file, this._register(new DiskFileSystemProvider(logService))); + + // Configuration + const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); + await configurationService.initialize(); + services.set(IConfigurationService, configurationService); + + services.set(IRequestService, new SyncDescriptor(RequestService)); + services.set(IDownloadService, new SyncDescriptor(DownloadService)); + services.set(ITelemetryService, NullTelemetryService); + services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService)); + services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService)); + + return new InstantiationService(services); + } + + private async doRun(extensionManagementCLIService: IExtensionManagementCLIService): Promise { + + // List Extensions + if (this.args['list-extensions']) { + return extensionManagementCLIService.listExtensions(!!this.args['show-versions'], this.args['category']); + } + + // Install Extension + else if (this.args['install-extension'] || this.args['install-builtin-extension']) { + return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.args['install-extension'] || []), this.args['install-builtin-extension'] || [], !!this.args['do-not-sync'], !!this.args['force']); + } + + // Uninstall Extension + else if (this.args['uninstall-extension']) { + return extensionManagementCLIService.uninstallExtensions(this.asExtensionIdOrVSIX(this.args['uninstall-extension']), !!this.args['force']); + } + + // Locate Extension + else if (this.args['locate-extension']) { + return extensionManagementCLIService.locateExtension(this.args['locate-extension']); + } + } + + private asExtensionIdOrVSIX(inputs: string[]): (string | URI)[] { + return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); + } +} + +function eventuallyExit(code: number): void { + setTimeout(() => process.exit(code), 0); +} + +export async function run(args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise { + const cliMain = new CliMain(args, REMOTE_DATA_FOLDER); + try { + await cliMain.run(); + eventuallyExit(0); + } catch (err) { + eventuallyExit(1); + } finally { + cliMain.dispose(); + } +} diff --git a/src/vs/server/remoteExtensionHostAgentServer.ts b/src/vs/server/remoteExtensionHostAgentServer.ts new file mode 100644 index 0000000000000..f533969fbca56 --- /dev/null +++ b/src/vs/server/remoteExtensionHostAgentServer.ts @@ -0,0 +1,1039 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as net from 'net'; +import * as url from 'url'; +import { release, hostname } from 'os'; +import * as perf from 'vs/base/common/performance'; +import { performance } from 'perf_hooks'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; +import { Promises } from 'vs/base/node/pfs'; +import { findFreePort } from 'vs/base/node/ports'; +import * as platform from 'vs/base/common/platform'; +import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { ConnectionType, ConnectionTypeRequest, ErrorMessage, HandshakeMessage, IRemoteExtensionHostStartParams, ITunnelConnectionStartParams, SignRequest } from 'vs/platform/remote/common/remoteAgentConnection'; +import { ExtensionHostConnection } from 'vs/server/extensionHostConnection'; +import { ManagementConnection } from 'vs/server/remoteExtensionManagement'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { ILogService, LogLevel, AbstractLogger, DEFAULT_LOG_LEVEL, MultiplexLogService, getLogLevel, LogService } from 'vs/platform/log/common/log'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import product from 'vs/platform/product/common/product'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { RequestService } from 'vs/platform/request/node/requestService'; +import { ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; +import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; +import { ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; +import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; +import { getMachineId } from 'vs/base/node/id'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IPCServer, ClientConnectionEvent, IMessagePassingProtocol, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; +import { Emitter, Event } from 'vs/base/common/event'; +import { RemoteAgentEnvironmentChannel } from 'vs/server/remoteAgentEnvironmentImpl'; +import { RemoteAgentFileSystemChannel } from 'vs/server/remoteAgentFileSystemImpl'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; +import { RequestChannel } from 'vs/platform/request/common/requestIpc'; +import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import ErrorTelemetry from 'vs/platform/telemetry/node/errorTelemetry'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; +import { LogLevelChannel } from 'vs/platform/log/common/logIpc'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { WebClientServer, serveError, serveFile } from 'vs/server/webClientServer'; +import { URI } from 'vs/base/common/uri'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/serverEnvironmentService'; +import { basename, dirname, join } from 'vs/base/common/path'; +import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { RemoteTerminalChannel } from 'vs/server/remoteTerminalChannel'; +import { LoaderStats } from 'vs/base/common/amd'; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; +import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; +import { IPtyService, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; +import { IRemoteTelemetryService, RemoteNullTelemetryService, RemoteTelemetryService } from 'vs/server/remoteTelemetryService'; + +const SHUTDOWN_TIMEOUT = 5 * 60 * 1000; + +const eventPrefix = 'monacoworkbench'; + +class SocketServer extends IPCServer { + + private _onDidConnectEmitter: Emitter; + + constructor() { + const emitter = new Emitter(); + super(emitter.event); + this._onDidConnectEmitter = emitter; + } + + public acceptConnection(protocol: IMessagePassingProtocol, onDidClientDisconnect: Event): void { + this._onDidConnectEmitter.fire({ protocol, onDidClientDisconnect }); + } +} + +function twodigits(n: number): string { + if (n < 10) { + return `0${n}`; + } + return String(n); +} + +function now(): string { + const date = new Date(); + return `${twodigits(date.getHours())}:${twodigits(date.getMinutes())}:${twodigits(date.getSeconds())}`; +} + +class ServerLogService extends AbstractLogger implements ILogService { + _serviceBrand: undefined; + private useColors: boolean; + + constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { + super(); + this.setLevel(logLevel); + this.useColors = Boolean(process.stdout.isTTY); + } + + trace(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Trace) { + if (this.useColors) { + console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.log(`[${now()}]`, message, ...args); + } + } + } + + debug(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Debug) { + if (this.useColors) { + console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.log(`[${now()}]`, message, ...args); + } + } + } + + info(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Info) { + if (this.useColors) { + console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.log(`[${now()}]`, message, ...args); + } + } + } + + warn(message: string | Error, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Warning) { + if (this.useColors) { + console.warn(`\x1b[93m[${now()}]\x1b[0m`, message, ...args); + } else { + console.warn(`[${now()}]`, message, ...args); + } + } + } + + error(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Error) { + if (this.useColors) { + console.error(`\x1b[91m[${now()}]\x1b[0m`, message, ...args); + } else { + console.error(`[${now()}]`, message, ...args); + } + } + } + + critical(message: string, ...args: any[]): void { + if (this.getLevel() <= LogLevel.Critical) { + if (this.useColors) { + console.error(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); + } else { + console.error(`[${now()}]`, message, ...args); + } + } + } + + override dispose(): void { + // noop + } + + flush(): void { + // noop + } +} + +export type ServerListenOptions = { host?: string; port?: number; socketPath?: string }; + +declare module vsda { + // the signer is a native module that for historical reasons uses a lower case class name + // eslint-disable-next-line @typescript-eslint/naming-convention + export class signer { + sign(arg: string): string; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + export class validator { + createNewMessage(arg: string): string; + validate(arg: string): 'ok' | 'error'; + } +} + +export class RemoteExtensionHostAgentServer extends Disposable { + + private readonly _logService: ILogService; + private readonly _socketServer: SocketServer; + private readonly _uriTransformerCache: { [remoteAuthority: string]: IURITransformer; }; + private readonly _extHostConnections: { [reconnectionToken: string]: ExtensionHostConnection; }; + private readonly _managementConnections: { [reconnectionToken: string]: ManagementConnection; }; + private readonly _allReconnectionTokens: Set; + private readonly _webClientServer: WebClientServer | null; + + private shutdownTimer: NodeJS.Timer | undefined; + + constructor( + private readonly _environmentService: IServerEnvironmentService, + private readonly _productService: IProductService, + private readonly _connectionToken: string, + private readonly _connectionTokenIsMandatory: boolean, + hasWebClient: boolean, + REMOTE_DATA_FOLDER: string + ) { + super(); + + const logService = getOrCreateSpdLogService(this._environmentService); + logService.trace(`Remote configuration data at ${REMOTE_DATA_FOLDER}`); + logService.trace('process arguments:', this._environmentService.args); + + this._logService = new MultiplexLogService([new ServerLogService(getLogLevel(this._environmentService)), logService]); + this._socketServer = new SocketServer(); + this._uriTransformerCache = Object.create(null); + this._extHostConnections = Object.create(null); + this._managementConnections = Object.create(null); + this._allReconnectionTokens = new Set(); + + if (hasWebClient) { + this._webClientServer = new WebClientServer(this._connectionToken, this._environmentService, this._logService); + } else { + this._webClientServer = null; + } + this._logService.info(`Extension host agent started.`); + } + + public async initialize(): Promise<{ telemetryService: ITelemetryService; }> { + const services = await this._createServices(); + setTimeout(() => this._cleanupOlderLogs(this._environmentService.logsPath).then(null, err => this._logService.error(err)), 10000); + return services; + } + + private async _createServices(): Promise<{ telemetryService: ITelemetryService; }> { + const services = new ServiceCollection(); + + // ExtensionHost Debug broadcast service + this._socketServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel()); + + // TODO: @Sandy @Joao need dynamic context based router + const router = new StaticRouter(ctx => ctx.clientId === 'renderer'); + this._socketServer.registerChannel('logger', new LogLevelChannel(this._logService)); + + services.set(IEnvironmentService, this._environmentService); + services.set(INativeEnvironmentService, this._environmentService); + + services.set(ILogService, this._logService); + services.set(IProductService, this._productService); + + // Files + const fileService = this._register(new FileService(this._logService)); + services.set(IFileService, fileService); + fileService.registerProvider(Schemas.file, this._register(new DiskFileSystemProvider(this._logService))); + + const configurationService = new ConfigurationService(this._environmentService.machineSettingsResource, fileService); + services.set(IConfigurationService, configurationService); + services.set(IRequestService, new SyncDescriptor(RequestService)); + + let appInsightsAppender: ITelemetryAppender = NullAppender; + if (!this._environmentService.args['disable-telemetry'] && product.enableTelemetry) { + if (product.aiConfig && product.aiConfig.asimovKey) { + appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey); + this._register(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data + } + + const machineId = await getMachineId(); + const config: ITelemetryServiceConfig = { + appenders: [appInsightsAppender], + commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, product.commit, product.version + '-remote', machineId, product.msftInternalDomains, this._environmentService.installSourcePath, 'remoteAgent'), + piiPaths: [this._environmentService.appRoot] + }; + + services.set(IRemoteTelemetryService, new SyncDescriptor(RemoteTelemetryService, [config])); + } else { + services.set(IRemoteTelemetryService, RemoteNullTelemetryService); + } + + services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); + + const downloadChannel = this._socketServer.getChannel('download', router); + services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => this._getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */)); + + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + + const instantiationService = new InstantiationService(services); + services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService)); + + const extensionManagementCLIService = instantiationService.createInstance(ExtensionManagementCLIService); + services.set(IExtensionManagementCLIService, extensionManagementCLIService); + + const ptyService = instantiationService.createInstance( + PtyHostService, + { + GraceTime: ProtocolConstants.ReconnectionGraceTime, + ShortGraceTime: ProtocolConstants.ReconnectionShortGraceTime, + scrollback: configurationService.getValue(TerminalSettingId.PersistentSessionScrollback) ?? 100 + } + ); + services.set(IPtyService, ptyService); + + return instantiationService.invokeFunction(accessor => { + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(this._connectionToken, this._environmentService, extensionManagementCLIService, this._logService, accessor.get(IRemoteTelemetryService), appInsightsAppender); + this._socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); + + this._socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(this._environmentService, this._logService, ptyService)); + + const remoteFileSystemChannel = new RemoteAgentFileSystemChannel(this._logService, this._environmentService); + this._socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); + + this._socketServer.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); + + const extensionManagementService = accessor.get(IExtensionManagementService); + const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => this._getUriTransformer(ctx.remoteAuthority)); + this._socketServer.registerChannel('extensions', channel); + + // clean up deprecated extensions + (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); + + this._register(new ErrorTelemetry(accessor.get(ITelemetryService))); + + return { + telemetryService: accessor.get(ITelemetryService) + }; + }); + } + + private _getUriTransformer(remoteAuthority: string): IURITransformer { + if (!this._uriTransformerCache[remoteAuthority]) { + this._uriTransformerCache[remoteAuthority] = createRemoteURITransformer(remoteAuthority); + } + return this._uriTransformerCache[remoteAuthority]; + } + + public async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { + // Only serve GET requests + if (req.method !== 'GET') { + return serveError(req, res, 405, `Unsupported method ${req.method}`); + } + + if (!req.url) { + return serveError(req, res, 400, `Bad request.`); + } + + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname; + + if (!pathname) { + return serveError(req, res, 400, `Bad request.`); + } + + // Version + if (pathname === '/version') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + return res.end(product.commit || ''); + } + + // Delay shutdown + if (pathname === '/delay-shutdown') { + this._delayShutdown(); + res.writeHead(200); + return res.end('OK'); + } + + if (pathname === '/vscode-remote-resource') { + // Handle HTTP requests for resources rendered in the rich client (images, fonts, etc.) + // These resources could be files shipped with extensions or even workspace files. + if (parsedUrl.query['tkn'] !== this._connectionToken) { + return serveError(req, res, 403, `Forbidden.`); + } + + const desiredPath = parsedUrl.query['path']; + if (typeof desiredPath !== 'string') { + return serveError(req, res, 400, `Bad request.`); + } + + let filePath: string; + try { + filePath = URI.from({ scheme: Schemas.file, path: desiredPath }).fsPath; + } catch (err) { + return serveError(req, res, 400, `Bad request.`); + } + + const responseHeaders: Record = Object.create(null); + if (this._environmentService.isBuilt) { + if (isEqualOrParent(filePath, this._environmentService.builtinExtensionsPath, !platform.isLinux) + || isEqualOrParent(filePath, this._environmentService.extensionsPath, !platform.isLinux) + ) { + responseHeaders['Cache-Control'] = 'public, max-age=31536000'; + } + } + return serveFile(this._logService, req, res, filePath, responseHeaders); + } + + // workbench web UI + if (this._webClientServer) { + this._webClientServer.handle(req, res, parsedUrl); + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + return res.end('Not found'); + } + + public handleUpgrade(req: http.IncomingMessage, socket: net.Socket) { + let reconnectionToken = generateUuid(); + let isReconnection = false; + let skipWebSocketFrames = false; + + if (req.url) { + const query = url.parse(req.url, true).query; + if (typeof query.reconnectionToken === 'string') { + reconnectionToken = query.reconnectionToken; + } + if (query.reconnection === 'true') { + isReconnection = true; + } + if (query.skipWebSocketFrames === 'true') { + skipWebSocketFrames = true; + } + } + + if (req.headers['upgrade'] !== 'websocket') { + socket.end('HTTP/1.1 400 Bad Request'); + return; + } + + // https://tools.ietf.org/html/rfc6455#section-4 + const requestNonce = req.headers['sec-websocket-key']; + const hash = crypto.createHash('sha1'); + hash.update(requestNonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'); + const responseNonce = hash.digest('base64'); + + const responseHeaders = [ + `HTTP/1.1 101 Switching Protocols`, + `Upgrade: websocket`, + `Connection: Upgrade`, + `Sec-WebSocket-Accept: ${responseNonce}` + ]; + + // See https://tools.ietf.org/html/rfc7692#page-12 + let permessageDeflate = false; + if (!skipWebSocketFrames && !this._environmentService.args['disable-websocket-compression'] && req.headers['sec-websocket-extensions']) { + const websocketExtensionOptions = Array.isArray(req.headers['sec-websocket-extensions']) ? req.headers['sec-websocket-extensions'] : [req.headers['sec-websocket-extensions']]; + for (const websocketExtensionOption of websocketExtensionOptions) { + if (/\b((server_max_window_bits)|(server_no_context_takeover)|(client_no_context_takeover))\b/.test(websocketExtensionOption)) { + // sorry, the server does not support zlib parameter tweaks + continue; + } + if (/\b(permessage-deflate)\b/.test(websocketExtensionOption)) { + permessageDeflate = true; + responseHeaders.push(`Sec-WebSocket-Extensions: permessage-deflate`); + break; + } + if (/\b(x-webkit-deflate-frame)\b/.test(websocketExtensionOption)) { + permessageDeflate = true; + responseHeaders.push(`Sec-WebSocket-Extensions: x-webkit-deflate-frame`); + break; + } + } + } + + socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); + + // Never timeout this socket due to inactivity! + socket.setTimeout(0); + // Finally! + + if (skipWebSocketFrames) { + this._handleWebSocketConnection(new NodeSocket(socket), isReconnection, reconnectionToken); + } else { + this._handleWebSocketConnection(new WebSocketNodeSocket(new NodeSocket(socket), permessageDeflate, null, true), isReconnection, reconnectionToken); + } + } + + public handleServerError(err: Error): void { + this._logService.error(`Error occurred in server`); + this._logService.error(err); + } + + // Eventually cleanup + /** + * Cleans up older logs, while keeping the 10 most recent ones. + */ + private async _cleanupOlderLogs(logsPath: string): Promise { + const currentLog = basename(logsPath); + const logsRoot = dirname(logsPath); + const children = await Promises.readdir(logsRoot); + const allSessions = children.filter(name => /^\d{8}T\d{6}$/.test(name)); + const oldSessions = allSessions.sort().filter((d) => d !== currentLog); + const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9)); + + await Promise.all(toDelete.map(name => Promises.rm(join(logsRoot, name)))); + } + + private _getRemoteAddress(socket: NodeSocket | WebSocketNodeSocket): string { + let _socket: net.Socket; + if (socket instanceof NodeSocket) { + _socket = socket.socket; + } else { + _socket = socket.socket.socket; + } + return _socket.remoteAddress || ``; + } + + private async _rejectWebSocketConnection(logPrefix: string, protocol: PersistentProtocol, reason: string): Promise { + const socket = protocol.getSocket(); + this._logService.error(`${logPrefix} ${reason}.`); + const errMessage: ErrorMessage = { + type: 'error', + reason: reason + }; + protocol.sendControl(VSBuffer.fromString(JSON.stringify(errMessage))); + protocol.dispose(); + await socket.drain(); + socket.dispose(); + } + + /** + * NOTE: Avoid using await in this method! + * The problem is that await introduces a process.nextTick due to the implicit Promise.then + * This can lead to some bytes being interpreted and a control message being emitted before the next listener has a chance to be registered. + */ + private _handleWebSocketConnection(socket: NodeSocket | WebSocketNodeSocket, isReconnection: boolean, reconnectionToken: string): void { + const remoteAddress = this._getRemoteAddress(socket); + const logPrefix = `[${remoteAddress}][${reconnectionToken.substr(0, 8)}]`; + const protocol = new PersistentProtocol(socket); + + let validator: vsda.validator; + let signer: vsda.signer; + try { + const vsdaMod = require.__$__nodeRequire('vsda'); + validator = new vsdaMod.validator(); + signer = new vsdaMod.signer(); + } catch (e) { + } + + const enum State { + WaitingForAuth, + WaitingForConnectionType, + Done, + Error + } + let state = State.WaitingForAuth; + + const rejectWebSocketConnection = (msg: string) => { + state = State.Error; + listener.dispose(); + this._rejectWebSocketConnection(logPrefix, protocol, msg); + }; + + const listener = protocol.onControlMessage((raw) => { + if (state === State.WaitingForAuth) { + let msg1: HandshakeMessage; + try { + msg1 = JSON.parse(raw.toString()); + } catch (err) { + return rejectWebSocketConnection(`Malformed first message`); + } + if (msg1.type !== 'auth') { + return rejectWebSocketConnection(`Invalid first message`); + } + + if (this._connectionTokenIsMandatory && msg1.auth !== this._connectionToken) { + return rejectWebSocketConnection(`Unauthorized client refused: auth mismatch`); + } + + // Send `sign` request + let signedData = generateUuid(); + if (signer) { + try { + signedData = signer.sign(msg1.data); + } catch (e) { + } + } + let someText = generateUuid(); + if (validator) { + try { + someText = validator.createNewMessage(someText); + } catch (e) { + } + } + const signRequest: SignRequest = { + type: 'sign', + data: someText, + signedData: signedData + }; + protocol.sendControl(VSBuffer.fromString(JSON.stringify(signRequest))); + + state = State.WaitingForConnectionType; + + } else if (state === State.WaitingForConnectionType) { + + let msg2: HandshakeMessage; + try { + msg2 = JSON.parse(raw.toString()); + } catch (err) { + return rejectWebSocketConnection(`Malformed second message`); + } + if (msg2.type !== 'connectionType') { + return rejectWebSocketConnection(`Invalid second message`); + } + if (typeof msg2.signedData !== 'string') { + return rejectWebSocketConnection(`Invalid second message field type`); + } + + const rendererCommit = msg2.commit; + const myCommit = product.commit; + if (rendererCommit && myCommit) { + // Running in the built version where commits are defined + if (rendererCommit !== myCommit) { + return rejectWebSocketConnection(`Client refused: version mismatch`); + } + } + + let valid = false; + if (!validator) { + valid = true; + } else if (msg2.signedData === this._connectionToken) { + // web client + valid = true; + } else { + try { + valid = validator.validate(msg2.signedData) === 'ok'; + } catch (e) { + } + } + + if (!valid) { + if (this._environmentService.isBuilt) { + return rejectWebSocketConnection(`Unauthorized client refused`); + } else { + this._logService.error(`${logPrefix} Unauthorized client handshake failed but we proceed because of dev mode.`); + } + } + + // We have received a new connection. + // This indicates that the server owner has connectivity. + // Therefore we will shorten the reconnection grace period for disconnected connections! + for (let key in this._managementConnections) { + const managementConnection = this._managementConnections[key]; + managementConnection.shortenReconnectionGraceTimeIfNecessary(); + } + for (let key in this._extHostConnections) { + const extHostConnection = this._extHostConnections[key]; + extHostConnection.shortenReconnectionGraceTimeIfNecessary(); + } + + state = State.Done; + listener.dispose(); + this._handleConnectionType(remoteAddress, logPrefix, protocol, socket, isReconnection, reconnectionToken, msg2); + } + }); + } + + private async _handleConnectionType(remoteAddress: string, _logPrefix: string, protocol: PersistentProtocol, socket: NodeSocket | WebSocketNodeSocket, isReconnection: boolean, reconnectionToken: string, msg: ConnectionTypeRequest): Promise { + const logPrefix = ( + msg.desiredConnectionType === ConnectionType.Management + ? `${_logPrefix}[ManagementConnection]` + : msg.desiredConnectionType === ConnectionType.ExtensionHost + ? `${_logPrefix}[ExtensionHostConnection]` + : _logPrefix + ); + + if (msg.desiredConnectionType === ConnectionType.Management) { + // This should become a management connection + + if (isReconnection) { + // This is a reconnection + if (!this._managementConnections[reconnectionToken]) { + if (!this._allReconnectionTokens.has(reconnectionToken)) { + // This is an unknown reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (never seen)`); + } else { + // This is a connection that was seen in the past, but is no longer valid + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (seen before)`); + } + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' }))); + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + this._managementConnections[reconnectionToken].acceptReconnection(remoteAddress, socket, dataChunk); + + } else { + // This is a fresh connection + if (this._managementConnections[reconnectionToken]) { + // Cannot have two concurrent connections using the same reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`); + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' }))); + const con = new ManagementConnection(this._logService, reconnectionToken, remoteAddress, protocol); + this._socketServer.acceptConnection(con.protocol, con.onClose); + this._managementConnections[reconnectionToken] = con; + this._allReconnectionTokens.add(reconnectionToken); + con.onClose(() => { + delete this._managementConnections[reconnectionToken]; + }); + + } + + } else if (msg.desiredConnectionType === ConnectionType.ExtensionHost) { + + // This should become an extension host connection + const startParams0 = msg.args || { language: 'en' }; + const startParams = await this._updateWithFreeDebugPort(startParams0); + + if (startParams.port) { + this._logService.trace(`${logPrefix} - startParams debug port ${startParams.port}`); + } + this._logService.trace(`${logPrefix} - startParams language: ${startParams.language}`); + this._logService.trace(`${logPrefix} - startParams env: ${JSON.stringify(startParams.env)}`); + + if (isReconnection) { + // This is a reconnection + if (!this._extHostConnections[reconnectionToken]) { + if (!this._allReconnectionTokens.has(reconnectionToken)) { + // This is an unknown reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (never seen)`); + } else { + // This is a connection that was seen in the past, but is no longer valid + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (seen before)`); + } + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {}))); + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + this._extHostConnections[reconnectionToken].acceptReconnection(remoteAddress, socket, dataChunk); + + } else { + // This is a fresh connection + if (this._extHostConnections[reconnectionToken]) { + // Cannot have two concurrent connections using the same reconnection token + return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`); + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {}))); + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + const con = new ExtensionHostConnection(this._environmentService, this._logService, reconnectionToken, remoteAddress, socket, dataChunk); + this._extHostConnections[reconnectionToken] = con; + this._allReconnectionTokens.add(reconnectionToken); + con.onClose(() => { + delete this._extHostConnections[reconnectionToken]; + this._onDidCloseExtHostConnection(); + }); + con.start(startParams); + } + + } else if (msg.desiredConnectionType === ConnectionType.Tunnel) { + + const tunnelStartParams = msg.args; + this._createTunnel(protocol, tunnelStartParams); + + } else { + + return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown initial data received`); + + } + } + + private async _createTunnel(protocol: PersistentProtocol, tunnelStartParams: ITunnelConnectionStartParams): Promise { + const remoteSocket = (protocol.getSocket()).socket; + const dataChunk = protocol.readEntireBuffer(); + protocol.dispose(); + + remoteSocket.pause(); + const localSocket = await this._connectTunnelSocket(tunnelStartParams.host, tunnelStartParams.port); + + if (dataChunk.byteLength > 0) { + localSocket.write(dataChunk.buffer); + } + + localSocket.on('end', () => remoteSocket.end()); + localSocket.on('close', () => remoteSocket.end()); + localSocket.on('error', () => remoteSocket.destroy()); + remoteSocket.on('end', () => localSocket.end()); + remoteSocket.on('close', () => localSocket.end()); + remoteSocket.on('error', () => localSocket.destroy()); + + localSocket.pipe(remoteSocket); + remoteSocket.pipe(localSocket); + } + + private _connectTunnelSocket(host: string, port: number): Promise { + return new Promise((c, e) => { + const socket = net.createConnection( + { + host: host, + port: port + }, () => { + socket.removeListener('error', e); + socket.pause(); + c(socket); + } + ); + + socket.once('error', e); + }); + } + + private _updateWithFreeDebugPort(startParams: IRemoteExtensionHostStartParams): Thenable { + if (typeof startParams.port === 'number') { + return findFreePort(startParams.port, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */).then(freePort => { + startParams.port = freePort; + return startParams; + }); + } + // No port clear debug configuration. + startParams.debugId = undefined; + startParams.port = undefined; + startParams.break = undefined; + return Promise.resolve(startParams); + } + + private async _onDidCloseExtHostConnection(): Promise { + if (!this._environmentService.args['enable-remote-auto-shutdown']) { + return; + } + + this._cancelShutdown(); + + const hasActiveExtHosts = !!Object.keys(this._extHostConnections).length; + if (!hasActiveExtHosts) { + console.log('Last EH closed, waiting before shutting down'); + this._logService.info('Last EH closed, waiting before shutting down'); + this._waitThenShutdown(); + } + } + + private _waitThenShutdown(): void { + if (!this._environmentService.args['enable-remote-auto-shutdown']) { + return; + } + + if (this._environmentService.args['remote-auto-shutdown-without-delay']) { + this._shutdown(); + } else { + this.shutdownTimer = setTimeout(() => { + this.shutdownTimer = undefined; + + this._shutdown(); + }, SHUTDOWN_TIMEOUT); + } + } + + private _shutdown(): void { + const hasActiveExtHosts = !!Object.keys(this._extHostConnections).length; + if (hasActiveExtHosts) { + console.log('New EH opened, aborting shutdown'); + this._logService.info('New EH opened, aborting shutdown'); + return; + } else { + console.log('Last EH closed, shutting down'); + this._logService.info('Last EH closed, shutting down'); + this.dispose(); + process.exit(0); + } + } + + /** + * If the server is in a shutdown timeout, cancel it and start over + */ + private _delayShutdown(): void { + if (this.shutdownTimer) { + console.log('Got delay-shutdown request while in shutdown timeout, delaying'); + this._logService.info('Got delay-shutdown request while in shutdown timeout, delaying'); + this._cancelShutdown(); + this._waitThenShutdown(); + } + } + + private _cancelShutdown(): void { + if (this.shutdownTimer) { + console.log('Cancelling previous shutdown timeout'); + this._logService.info('Cancelling previous shutdown timeout'); + clearTimeout(this.shutdownTimer); + this.shutdownTimer = undefined; + } + } +} + +function parseConnectionToken(args: ServerParsedArgs): { connectionToken: string; connectionTokenIsMandatory: boolean; } { + if (args['connection-secret']) { + if (args['connectionToken']) { + console.warn(`Please do not use the argument connectionToken at the same time as connection-secret.`); + process.exit(1); + } + let rawConnectionToken = fs.readFileSync(args['connection-secret']).toString(); + rawConnectionToken = rawConnectionToken.replace(/\r?\n$/, ''); + if (!/^[0-9A-Za-z\-]+$/.test(rawConnectionToken)) { + console.warn(`The secret defined in ${args['connection-secret']} does not adhere to the characters 0-9, a-z, A-Z or -.`); + process.exit(1); + } + return { connectionToken: rawConnectionToken, connectionTokenIsMandatory: true }; + } else { + return { connectionToken: args['connectionToken'] || generateUuid(), connectionTokenIsMandatory: false }; + } +} + +export interface IServerAPI { + /** + * Do not remove!!. Called from vs/server/main.js + */ + handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise; + /** + * Do not remove!!. Called from vs/server/main.js + */ + handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void; + /** + * Do not remove!!. Called from vs/server/main.js + */ + handleServerError(err: Error): void; + /** + * Do not remove!!. Called from vs/server/main.js + */ + dispose(): void; +} + +export async function createServer(address: string | net.AddressInfo | null, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise { + const productService = { _serviceBrand: undefined, ...product }; + const environmentService = new ServerEnvironmentService(args, productService); + + // + // On Windows, exit early with warning message to users about potential security issue + // if there is node_modules folder under home drive or Users folder. + // + if (process.platform === 'win32' && process.env.HOMEDRIVE && process.env.HOMEPATH) { + const homeDirModulesPath = join(process.env.HOMEDRIVE, 'node_modules'); + const userDir = dirname(join(process.env.HOMEDRIVE, process.env.HOMEPATH)); + const userDirModulesPath = join(userDir, 'node_modules'); + if (fs.existsSync(homeDirModulesPath) || fs.existsSync(userDirModulesPath)) { + const message = ` + +* +* !!!! Server terminated due to presence of CVE-2020-1416 !!!! +* +* Please remove the following directories and re-try +* ${homeDirModulesPath} +* ${userDirModulesPath} +* +* For more information on the vulnerability https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1416 +* + +`; + const logService = getOrCreateSpdLogService(environmentService); + logService.warn(message); + console.warn(message); + process.exit(0); + } + } + + const { connectionToken, connectionTokenIsMandatory } = parseConnectionToken(args); + const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/code/browser/workbench/workbench.html', require).fsPath); + + if (hasWebClient && address && typeof address !== 'string') { + // ships the web ui! + console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}/?tkn=${connectionToken}`); + } + + const remoteExtensionHostAgentServer = new RemoteExtensionHostAgentServer(environmentService, productService, connectionToken, connectionTokenIsMandatory, hasWebClient, REMOTE_DATA_FOLDER); + const services = await remoteExtensionHostAgentServer.initialize(); + const { telemetryService } = services; + + perf.mark('code/server/ready'); + const currentTime = performance.now(); + const vscodeServerStartTime: number = (global).vscodeServerStartTime; + const vscodeServerListenTime: number = (global).vscodeServerListenTime; + const vscodeServerCodeLoadedTime: number = (global).vscodeServerCodeLoadedTime; + + type ServerStartClassification = { + startTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + startedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + codeLoadedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + readyTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + }; + type ServerStartEvent = { + startTime: number; + startedTime: number; + codeLoadedTime: number; + readyTime: number; + }; + telemetryService.publicLog2('serverStart', { + startTime: vscodeServerStartTime, + startedTime: vscodeServerListenTime, + codeLoadedTime: vscodeServerCodeLoadedTime, + readyTime: currentTime + }); + + if (args['print-startup-performance']) { + const stats = LoaderStats.get(); + let output = ''; + output += '\n\n### Load AMD-module\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.amdLoad); + output += '\n\n### Load commonjs-module\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.nodeRequire); + output += '\n\n### Invoke AMD-module factory\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.amdInvoke); + output += '\n\n### Invoke commonjs-module\n'; + output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.nodeEval); + output += `Start-up time: ${vscodeServerListenTime - vscodeServerStartTime}\n`; + output += `Code loading time: ${vscodeServerCodeLoadedTime - vscodeServerStartTime}\n`; + output += `Initialized time: ${currentTime - vscodeServerStartTime}\n`; + output += `\n`; + console.log(output); + } + return remoteExtensionHostAgentServer; +} + +const getOrCreateSpdLogService: (environmentService: IServerEnvironmentService) => ILogService = (function () { + let _logService: ILogService | null; + return function getLogService(environmentService: IServerEnvironmentService): ILogService { + if (!_logService) { + _logService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, getLogLevel(environmentService))); + } + return _logService; + }; +})(); diff --git a/src/vs/server/remoteExtensionHostProcess.ts b/src/vs/server/remoteExtensionHostProcess.ts new file mode 100644 index 0000000000000..4c890beb60d47 --- /dev/null +++ b/src/vs/server/remoteExtensionHostProcess.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { startExtensionHostProcess } from 'vs/workbench/services/extensions/node/extensionHostProcessSetup'; + +startExtensionHostProcess().catch((err) => console.log(err)); diff --git a/src/vs/server/remoteExtensionManagement.ts b/src/vs/server/remoteExtensionManagement.ts new file mode 100644 index 0000000000000..841df360a6412 --- /dev/null +++ b/src/vs/server/remoteExtensionManagement.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PersistentProtocol, ProtocolConstants, ISocket } from 'vs/base/parts/ipc/common/ipc.net'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Emitter, Event } from 'vs/base/common/event'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ProcessTimeRunOnceScheduler } from 'vs/base/common/async'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; + +export interface IExtensionsManagementProcessInitData { + args: NativeParsedArgs; +} + +export function printTime(ms: number): string { + let h = 0; + let m = 0; + let s = 0; + if (ms >= 1000) { + s = Math.floor(ms / 1000); + ms -= s * 1000; + } + if (s >= 60) { + m = Math.floor(s / 60); + s -= m * 60; + } + if (m >= 60) { + h = Math.floor(m / 60); + m -= h * 60; + } + const _h = h ? `${h}h` : ``; + const _m = m ? `${m}m` : ``; + const _s = s ? `${s}s` : ``; + const _ms = ms ? `${ms}ms` : ``; + return `${_h}${_m}${_s}${_ms}`; +} + +export class ManagementConnection { + + private _onClose = new Emitter(); + public readonly onClose: Event = this._onClose.event; + + private readonly _reconnectionGraceTime: number; + private readonly _reconnectionShortGraceTime: number; + private _remoteAddress: string; + + public readonly protocol: PersistentProtocol; + private _disposed: boolean; + private _disconnectRunner1: ProcessTimeRunOnceScheduler; + private _disconnectRunner2: ProcessTimeRunOnceScheduler; + + constructor( + private readonly _logService: ILogService, + private readonly _reconnectionToken: string, + remoteAddress: string, + protocol: PersistentProtocol + ) { + this._reconnectionGraceTime = ProtocolConstants.ReconnectionGraceTime; + this._reconnectionShortGraceTime = ProtocolConstants.ReconnectionShortGraceTime; + this._remoteAddress = remoteAddress; + + this.protocol = protocol; + this._disposed = false; + this._disconnectRunner1 = new ProcessTimeRunOnceScheduler(() => { + this._log(`The reconnection grace time of ${printTime(this._reconnectionGraceTime)} has expired, so the connection will be disposed.`); + this._cleanResources(); + }, this._reconnectionGraceTime); + this._disconnectRunner2 = new ProcessTimeRunOnceScheduler(() => { + this._log(`The reconnection short grace time of ${printTime(this._reconnectionShortGraceTime)} has expired, so the connection will be disposed.`); + this._cleanResources(); + }, this._reconnectionShortGraceTime); + + this.protocol.onDidDispose(() => { + this._log(`The client has disconnected gracefully, so the connection will be disposed.`); + this._cleanResources(); + }); + this.protocol.onSocketClose(() => { + this._log(`The client has disconnected, will wait for reconnection ${printTime(this._reconnectionGraceTime)} before disposing...`); + // The socket has closed, let's give the renderer a certain amount of time to reconnect + this._disconnectRunner1.schedule(); + }); + + this._log(`New connection established.`); + } + + private _log(_str: string): void { + this._logService.info(`[${this._remoteAddress}][${this._reconnectionToken.substr(0, 8)}][ManagementConnection] ${_str}`); + } + + public shortenReconnectionGraceTimeIfNecessary(): void { + if (this._disconnectRunner2.isScheduled()) { + // we are disconnected and already running the short reconnection timer + return; + } + if (this._disconnectRunner1.isScheduled()) { + this._log(`Another client has connected, will shorten the wait for reconnection ${printTime(this._reconnectionShortGraceTime)} before disposing...`); + // we are disconnected and running the long reconnection timer + this._disconnectRunner2.schedule(); + } + } + + private _cleanResources(): void { + if (this._disposed) { + // already called + return; + } + this._disposed = true; + this._disconnectRunner1.dispose(); + this._disconnectRunner2.dispose(); + const socket = this.protocol.getSocket(); + this.protocol.sendDisconnect(); + this.protocol.dispose(); + socket.end(); + this._onClose.fire(undefined); + } + + public acceptReconnection(remoteAddress: string, socket: ISocket, initialDataChunk: VSBuffer): void { + this._remoteAddress = remoteAddress; + this._log(`The client has reconnected.`); + this._disconnectRunner1.cancel(); + this._disconnectRunner2.cancel(); + this.protocol.beginAcceptReconnection(socket, initialDataChunk); + this.protocol.endAcceptReconnection(); + } +} diff --git a/src/vs/server/remoteLanguagePacks.ts b/src/vs/server/remoteLanguagePacks.ts new file mode 100644 index 0000000000000..bb76b3c9e5db8 --- /dev/null +++ b/src/vs/server/remoteLanguagePacks.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { FileAccess } from 'vs/base/common/network'; +import * as path from 'vs/base/common/path'; + +import * as lp from 'vs/base/node/languagePacks'; +import product from 'vs/platform/product/common/product'; + +const metaData = path.join(FileAccess.asFileUri('', require).fsPath, 'nls.metadata.json'); +const _cache: Map> = new Map(); + +function exists(file: string) { + return new Promise(c => fs.exists(file, c)); +} + +export function getNLSConfiguration(language: string, userDataPath: string): Promise { + return exists(metaData).then((fileExists) => { + if (!fileExists || !product.commit) { + // console.log(`==> MetaData or commit unknown. Using default language.`); + return Promise.resolve({ locale: 'en', availableLanguages: {} }); + } + let key = `${language}||${userDataPath}`; + let result = _cache.get(key); + if (!result) { + result = lp.getNLSConfiguration(product.commit, userDataPath, metaData, language).then(value => { + if (InternalNLSConfiguration.is(value)) { + value._languagePackSupport = true; + } + return value; + }); + _cache.set(key, result); + } + return result; + }); +} + +export namespace InternalNLSConfiguration { + export function is(value: lp.NLSConfiguration): value is lp.InternalNLSConfiguration { + let candidate: lp.InternalNLSConfiguration = value as lp.InternalNLSConfiguration; + return candidate && typeof candidate._languagePackId === 'string'; + } +} diff --git a/src/vs/server/remoteTelemetryService.ts b/src/vs/server/remoteTelemetryService.ts new file mode 100644 index 0000000000000..db4e5ab2372fe --- /dev/null +++ b/src/vs/server/remoteTelemetryService.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; +import { NullTelemetryServiceShape } from 'vs/platform/telemetry/common/telemetryUtils'; + +export interface IRemoteTelemetryService extends ITelemetryService { + permanentlyDisableTelemetry(): void +} + +export class RemoteTelemetryService extends TelemetryService implements IRemoteTelemetryService { + private _isDisabled = false; + constructor( + config: ITelemetryServiceConfig, + @IConfigurationService _configurationService: IConfigurationService + ) { + super(config, _configurationService); + } + + override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLog(eventName, data, anonymizeFilePaths); + } + + override publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLog2(eventName, data, anonymizeFilePaths); + } + + override publicLogError(errorEventName: string, data?: ITelemetryData): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLogError(errorEventName, data); + } + + override publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyCheck): Promise { + if (this._isDisabled) { + return Promise.resolve(undefined); + } + return super.publicLogError2(eventName, data); + } + + permanentlyDisableTelemetry(): void { + this._isDisabled = true; + this.dispose(); + } +} + +export const RemoteNullTelemetryService = new class extends NullTelemetryServiceShape implements IRemoteTelemetryService { + permanentlyDisableTelemetry(): void { return; } // No-op, telemetry is already disabled +}; + +export const IRemoteTelemetryService = refineServiceDecorator(ITelemetryService); diff --git a/src/vs/server/remoteTerminalChannel.ts b/src/vs/server/remoteTerminalChannel.ts new file mode 100644 index 0000000000000..ac57503e81f17 --- /dev/null +++ b/src/vs/server/remoteTerminalChannel.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import { Emitter, Event } from 'vs/base/common/event'; +import { cloneAndChange } from 'vs/base/common/objects'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as path from 'vs/base/common/path'; +import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { createRandomIPCHandle } from 'vs/base/parts/ipc/node/ipc.net'; +import { ILogService } from 'vs/platform/log/common/log'; +import product from 'vs/platform/product/common/product'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IPtyService, IShellLaunchConfig, ITerminalProfile, ITerminalsLayoutInfo } from 'vs/platform/terminal/common/terminal'; +import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { CLIServerBase, ICommandsExecuter } from 'vs/workbench/api/node/extHostCLIServer'; +import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { ICreateTerminalProcessArguments, ICreateTerminalProcessResult, IWorkspaceFolderData } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { buildUserEnvironment } from 'vs/server/extensionHostConnection'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; + +class CustomVariableResolver extends AbstractVariableResolverService { + constructor( + env: platform.IProcessEnvironment, + workspaceFolders: IWorkspaceFolder[], + activeFileResource: URI | undefined, + resolvedVariables: { [name: string]: string; } + ) { + super({ + getFolderUri: (folderName: string): URI | undefined => { + const found = workspaceFolders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return workspaceFolders.length; + }, + getConfigurationValue: (folderUri: URI, section: string): string | undefined => { + return resolvedVariables[`config:${section}`]; + }, + getExecPath: (): string | undefined => { + return env['VSCODE_EXEC_PATH']; + }, + getAppRoot: (): string | undefined => { + return env['VSCODE_CWD']; + }, + getFilePath: (): string | undefined => { + if (activeFileResource) { + return path.normalize(activeFileResource.fsPath); + } + return undefined; + }, + getSelectedText: (): string | undefined => { + return resolvedVariables['selectedText']; + }, + getLineNumber: (): string | undefined => { + return resolvedVariables['lineNumber']; + } + }, undefined, Promise.resolve(env)); + } +} + +export class RemoteTerminalChannel extends Disposable implements IServerChannel { + + private _lastReqId = 0; + private readonly _pendingCommands = new Map void; + reject: (err: any) => void; + uriTransformer: IURITransformer; + }>(); + + private readonly _onExecuteCommand = this._register(new Emitter<{ reqId: number, commandId: string, commandArgs: any[] }>()); + readonly onExecuteCommand = this._onExecuteCommand.event; + + constructor( + private readonly _environmentService: IServerEnvironmentService, + private readonly _logService: ILogService, + private readonly _ptyService: IPtyService + ) { + super(); + } + + async call(ctx: RemoteAgentConnectionContext, command: string, args?: any): Promise { + switch (command) { + case '$restartPtyHost': return this._ptyService.restartPtyHost?.apply(this._ptyService, args); + + case '$createProcess': { + const uriTransformer = createRemoteURITransformer(ctx.remoteAuthority); + return this._createProcess(uriTransformer, args); + } + case '$attachToProcess': return this._ptyService.attachToProcess.apply(this._ptyService, args); + case '$detachFromProcess': return this._ptyService.detachFromProcess.apply(this._ptyService, args); + + case '$listProcesses': return this._ptyService.listProcesses.apply(this._ptyService, args); + case '$orphanQuestionReply': return this._ptyService.orphanQuestionReply.apply(this._ptyService, args); + case '$acceptPtyHostResolvedVariables': return this._ptyService.acceptPtyHostResolvedVariables?.apply(this._ptyService, args); + + case '$start': return this._ptyService.start.apply(this._ptyService, args); + case '$input': return this._ptyService.input.apply(this._ptyService, args); + case '$acknowledgeDataEvent': return this._ptyService.acknowledgeDataEvent.apply(this._ptyService, args); + case '$shutdown': return this._ptyService.shutdown.apply(this._ptyService, args); + case '$resize': return this._ptyService.resize.apply(this._ptyService, args); + case '$getInitialCwd': return this._ptyService.getInitialCwd.apply(this._ptyService, args); + case '$getCwd': return this._ptyService.getCwd.apply(this._ptyService, args); + + case '$processBinary': return this._ptyService.processBinary.apply(this._ptyService, args); + + case '$sendCommandResult': return this._sendCommandResult(args[0], args[1], args[2]); + case '$getDefaultSystemShell': return this._getDefaultSystemShell.apply(this, args); + case '$getProfiles': return this._getProfiles.apply(this, args); + case '$getEnvironment': return this._getEnvironment(); + case '$getWslPath': return this._getWslPath(args[0]); + case '$getTerminalLayoutInfo': return this._getTerminalLayoutInfo(args); + case '$setTerminalLayoutInfo': return this._setTerminalLayoutInfo(args); + case '$serializeTerminalState': return this._ptyService.serializeTerminalState.apply(this._ptyService, args); + case '$reviveTerminalProcesses': return this._ptyService.reviveTerminalProcesses.apply(this._ptyService, args); + case '$reduceConnectionGraceTime': return this._reduceConnectionGraceTime(); + case '$updateIcon': return this._ptyService.updateIcon.apply(this._ptyService, args); + case '$updateTitle': return this._ptyService.updateTitle.apply(this._ptyService, args); + case '$updateProperty': return this._ptyService.updateProperty.apply(this._ptyService, args); + case '$refreshProperty': return this._ptyService.refreshProperty.apply(this._ptyService, args); + case '$requestDetachInstance': return this._ptyService.requestDetachInstance(args[0], args[1]); + case '$acceptDetachedInstance': return this._ptyService.acceptDetachInstanceReply(args[0], args[1]); + } + + throw new Error(`IPC Command ${command} not found`); + } + + listen(_: any, event: string, arg: any): Event { + switch (event) { + case '$onPtyHostExitEvent': return this._ptyService.onPtyHostExit || Event.None; + case '$onPtyHostStartEvent': return this._ptyService.onPtyHostStart || Event.None; + case '$onPtyHostUnresponsiveEvent': return this._ptyService.onPtyHostUnresponsive || Event.None; + case '$onPtyHostResponsiveEvent': return this._ptyService.onPtyHostResponsive || Event.None; + case '$onPtyHostRequestResolveVariablesEvent': return this._ptyService.onPtyHostRequestResolveVariables || Event.None; + case '$onProcessDataEvent': return this._ptyService.onProcessData; + case '$onProcessExitEvent': return this._ptyService.onProcessExit; + case '$onProcessReadyEvent': return this._ptyService.onProcessReady; + case '$onProcessReplayEvent': return this._ptyService.onProcessReplay; + case '$onProcessTitleChangedEvent': return this._ptyService.onProcessTitleChanged; + case '$onProcessShellTypeChangedEvent': return this._ptyService.onProcessShellTypeChanged; + case '$onProcessOverrideDimensionsEvent': return this._ptyService.onProcessOverrideDimensions; + case '$onProcessResolvedShellLaunchConfigEvent': return this._ptyService.onProcessResolvedShellLaunchConfig; + case '$onProcessOrphanQuestion': return this._ptyService.onProcessOrphanQuestion; + case '$onProcessDidChangeHasChildProcesses': return this._ptyService.onProcessDidChangeHasChildProcesses; + case '$onExecuteCommand': return this.onExecuteCommand; + case '$onDidRequestDetach': return this._ptyService.onDidRequestDetach || Event.None; + case '$onDidChangeProperty': return this._ptyService.onDidChangeProperty; + default: + break; + } + + throw new Error('Not supported'); + } + + private async _createProcess(uriTransformer: IURITransformer, args: ICreateTerminalProcessArguments): Promise { + const shellLaunchConfig: IShellLaunchConfig = { + name: args.shellLaunchConfig.name, + executable: args.shellLaunchConfig.executable, + args: args.shellLaunchConfig.args, + cwd: ( + typeof args.shellLaunchConfig.cwd === 'string' || typeof args.shellLaunchConfig.cwd === 'undefined' + ? args.shellLaunchConfig.cwd + : URI.revive(uriTransformer.transformIncoming(args.shellLaunchConfig.cwd)) + ), + env: args.shellLaunchConfig.env, + useShellEnvironment: args.shellLaunchConfig.useShellEnvironment + }; + + + let baseEnv: platform.IProcessEnvironment; + if (args.shellLaunchConfig.useShellEnvironment) { + this._logService.trace('*'); + baseEnv = await buildUserEnvironment(args.resolverEnv, platform.language, false, this._environmentService, this._logService); + } else { + baseEnv = this._getEnvironment(); + } + this._logService.trace('baseEnv', baseEnv); + + const reviveWorkspaceFolder = (workspaceData: IWorkspaceFolderData): IWorkspaceFolder => { + return { + uri: URI.revive(uriTransformer.transformIncoming(workspaceData.uri)), + name: workspaceData.name, + index: workspaceData.index, + toResource: () => { + throw new Error('Not implemented'); + } + }; + }; + const workspaceFolders = args.workspaceFolders.map(reviveWorkspaceFolder); + const activeWorkspaceFolder = args.activeWorkspaceFolder ? reviveWorkspaceFolder(args.activeWorkspaceFolder) : undefined; + const activeFileResource = args.activeFileResource ? URI.revive(uriTransformer.transformIncoming(args.activeFileResource)) : undefined; + const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables); + const variableResolver = terminalEnvironment.createVariableResolver(activeWorkspaceFolder, process.env, customVariableResolver); + + // Get the initial cwd + const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService); + shellLaunchConfig.cwd = initialCwd; + + const envPlatformKey = platform.isWindows ? 'terminal.integrated.env.windows' : (platform.isMacintosh ? 'terminal.integrated.env.osx' : 'terminal.integrated.env.linux'); + const envFromConfig = args.configuration[envPlatformKey]; + const env = terminalEnvironment.createTerminalEnvironment( + shellLaunchConfig, + envFromConfig, + variableResolver, + product.version, + args.configuration['terminal.integrated.detectLocale'], + baseEnv + ); + + // Apply extension environment variable collections to the environment + if (!shellLaunchConfig.strictEnv) { + const entries: [string, IEnvironmentVariableCollection][] = []; + for (const [k, v] of args.envVariableCollections) { + entries.push([k, { map: deserializeEnvironmentVariableCollection(v) }]); + } + const envVariableCollections = new Map(entries); + const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); + mergedCollection.applyToProcessEnvironment(env); + } + + // Fork the process and listen for messages + this._logService.debug(`Terminal process launching on remote agent`, { shellLaunchConfig, initialCwd, cols: args.cols, rows: args.rows, env }); + + // Setup the CLI server to support forwarding commands run from the CLI + const ipcHandlePath = createRandomIPCHandle(); + env.VSCODE_IPC_HOOK_CLI = ipcHandlePath; + const commandsExecuter: ICommandsExecuter = { + executeCommand: (id: string, ...args: any[]): Promise => this._executeCommand(id, args, uriTransformer) + }; + const cliServer = new CLIServerBase(commandsExecuter, this._logService, ipcHandlePath); + + const id = await this._ptyService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows, args.unicodeVersion, env, baseEnv, false, args.shouldPersistTerminal, args.workspaceId, args.workspaceName); + this._ptyService.onProcessExit(e => e.id === id && cliServer.dispose()); + + return { + persistentTerminalId: id, + resolvedShellLaunchConfig: shellLaunchConfig + }; + } + + private _executeCommand(commandId: string, commandArgs: any[], uriTransformer: IURITransformer): Promise { + let resolve!: (data: any) => void; + let reject!: (err: any) => void; + const result = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + const reqId = ++this._lastReqId; + this._pendingCommands.set(reqId, { resolve, reject, uriTransformer }); + + const serializedCommandArgs = cloneAndChange(commandArgs, (obj) => { + if (obj && obj.$mid === 1) { + // this is UriComponents + return uriTransformer.transformOutgoing(obj); + } + if (obj && obj instanceof URI) { + return uriTransformer.transformOutgoingURI(obj); + } + return undefined; + }); + this._onExecuteCommand.fire({ + reqId, + commandId, + commandArgs: serializedCommandArgs + }); + + return result; + } + + private _sendCommandResult(reqId: number, isError: boolean, serializedPayload: any): void { + const data = this._pendingCommands.get(reqId); + if (!data) { + return; + } + this._pendingCommands.delete(reqId); + const payload = cloneAndChange(serializedPayload, (obj) => { + if (obj && obj.$mid === 1) { + // this is UriComponents + return data.uriTransformer.transformIncoming(obj); + } + return undefined; + }); + if (isError) { + data.reject(payload); + } else { + data.resolve(payload); + } + } + + private _getDefaultSystemShell(osOverride?: platform.OperatingSystem): Promise { + return this._ptyService.getDefaultSystemShell(osOverride); + } + + private async _getProfiles(workspaceId: string, profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise { + return this._ptyService.getProfiles?.(workspaceId, profiles, defaultProfile, includeDetectedProfiles) || []; + } + + private _getEnvironment(): platform.IProcessEnvironment { + return { ...process.env }; + } + + private _getWslPath(original: string): Promise { + return this._ptyService.getWslPath(original); + } + + private _setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void { + this._ptyService.setTerminalLayoutInfo(args); + } + + private async _getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise { + return this._ptyService.getTerminalLayoutInfo(args); + } + + private _reduceConnectionGraceTime(): Promise { + return this._ptyService.reduceConnectionGraceTime(); + } +} diff --git a/src/vs/server/remoteUriTransformer.ts b/src/vs/server/remoteUriTransformer.ts new file mode 100644 index 0000000000000..9fa8a624de92c --- /dev/null +++ b/src/vs/server/remoteUriTransformer.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URITransformer, IURITransformer, IRawURITransformer } from 'vs/base/common/uriIpc'; +import { FileAccess } from 'vs/base/common/network'; + +export const uriTransformerPath = FileAccess.asFileUri('vs/server/uriTransformer.js', require).fsPath; + +export function createRemoteURITransformer(remoteAuthority: string): IURITransformer { + const rawURITransformerFactory = require.__$__nodeRequire(uriTransformerPath); + const rawURITransformer = rawURITransformerFactory(remoteAuthority); + return new URITransformer(rawURITransformer); +} diff --git a/src/vs/server/serverEnvironmentService.ts b/src/vs/server/serverEnvironmentService.ts new file mode 100644 index 0000000000000..2e8a3ef939437 --- /dev/null +++ b/src/vs/server/serverEnvironmentService.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { OPTIONS, OptionDescriptions } from 'vs/platform/environment/node/argv'; +import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; + +export const serverOptions: OptionDescriptions = { + 'port': { type: 'string' }, + 'connectionToken': { type: 'string' }, + 'connection-secret': { type: 'string', description: nls.localize('connection-secret', "Path to file that contains the connection token. This will require that all incoming connections know the secret.") }, + 'host': { type: 'string' }, + 'socket-path': { type: 'string' }, + 'driver': { type: 'string' }, + 'start-server': { type: 'boolean' }, + 'print-startup-performance': { type: 'boolean' }, + 'print-ip-address': { type: 'boolean' }, + 'disable-websocket-compression': { type: 'boolean' }, + + 'fileWatcherPolling': { type: 'string' }, + + 'enable-remote-auto-shutdown': { type: 'boolean' }, + 'remote-auto-shutdown-without-delay': { type: 'boolean' }, + + 'without-browser-env-var': { type: 'boolean' }, + + 'disable-telemetry': OPTIONS['disable-telemetry'], + + 'extensions-dir': OPTIONS['extensions-dir'], + 'extensions-download-dir': OPTIONS['extensions-download-dir'], + 'install-extension': OPTIONS['install-extension'], + 'install-builtin-extension': OPTIONS['install-builtin-extension'], + 'uninstall-extension': OPTIONS['uninstall-extension'], + 'locate-extension': OPTIONS['locate-extension'], + 'list-extensions': OPTIONS['list-extensions'], + 'force': OPTIONS['force'], + 'show-versions': OPTIONS['show-versions'], + 'category': OPTIONS['category'], + 'do-not-sync': OPTIONS['do-not-sync'], + + 'force-disable-user-env': OPTIONS['force-disable-user-env'], + + 'folder': { type: 'string' }, + 'workspace': { type: 'string' }, + 'web-user-data-dir': { type: 'string' }, + 'use-host-proxy': { type: 'string' }, + 'enable-sync': { type: 'boolean' }, + 'github-auth': { type: 'string' }, + 'log': { type: 'string' }, + 'logsPath': { type: 'string' }, + + _: OPTIONS['_'] +}; + +export interface ServerParsedArgs { + port?: string; + connectionToken?: string; + /** + * A path to a filename which will be read on startup. + * Consider placing this file in a folder readable only by the same user (a `chmod 0700` directory). + * + * The contents of the file will be used as the connectionToken. Use only `[0-9A-Z\-]` as contents in the file. + * The file can optionally end in a `\n` which will be ignored. + * + * This secret must be communicated to any vscode instance via the resolver or embedder API. + */ + 'connection-secret'?: string; + host?: string; + 'socket-path'?: string; + driver?: string; + 'print-startup-performance'?: boolean; + 'print-ip-address'?: boolean; + 'disable-websocket-compression'?: boolean; + 'disable-telemetry'?: boolean; + fileWatcherPolling?: string; + 'start-server'?: boolean; + + 'enable-remote-auto-shutdown'?: boolean; + 'remote-auto-shutdown-without-delay'?: boolean; + + 'extensions-dir'?: string; + 'extensions-download-dir'?: string; + 'install-extension'?: string[]; + 'install-builtin-extension'?: string[]; + 'uninstall-extension'?: string[]; + 'list-extensions'?: boolean; + 'locate-extension'?: string[]; + 'show-versions'?: boolean; + 'category'?: string; + + 'force-disable-user-env'?: boolean; + 'use-host-proxy'?: string; + + 'without-browser-env-var'?: boolean; + + force?: boolean; // used by install-extension + 'do-not-sync'?: boolean; // used by install-extension + + 'user-data-dir'?: string; + 'builtin-extensions-dir'?: string; + + // web + workspace: string; + folder: string; + 'web-user-data-dir'?: string; + 'enable-sync'?: boolean; + 'github-auth'?: string; + 'log'?: string; + 'logsPath'?: string; + + _: string[]; +} + +export const IServerEnvironmentService = refineServiceDecorator(IEnvironmentService); + +export interface IServerEnvironmentService extends INativeEnvironmentService { + readonly args: ServerParsedArgs; +} + +export class ServerEnvironmentService extends NativeEnvironmentService implements IServerEnvironmentService { + override get args(): ServerParsedArgs { return super.args as ServerParsedArgs; } +} diff --git a/src/vs/server/uriTransformer.js b/src/vs/server/uriTransformer.js new file mode 100644 index 0000000000000..bc9ac7ada9411 --- /dev/null +++ b/src/vs/server/uriTransformer.js @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check + +/** + * ``` + * -------------------------------- + * | UI SIDE | AGENT SIDE | + * |---------------|--------------| + * | vscode-remote | file | + * | file | vscode-local | + * -------------------------------- + * ``` + */ +module.exports = function(remoteAuthority) { + return { + transformIncoming: (uri) => { + if (uri.scheme === 'vscode-remote') { + return { scheme: 'file', path: uri.path }; + } + if (uri.scheme === 'file') { + return { scheme: 'vscode-local', path: uri.path }; + } + return uri; + }, + + transformOutgoing: (uri) => { + if (uri.scheme === 'file') { + return { scheme: 'vscode-remote', authority: remoteAuthority, path: uri.path }; + } + if (uri.scheme === 'vscode-local') { + return { scheme: 'file', path: uri.path }; + } + return uri; + }, + + transformOutgoingScheme: (scheme) => { + if (scheme === 'file') { + return 'vscode-remote'; + } else if (scheme === 'vscode-local') { + return 'file'; + } + return scheme; + } + }; +}; diff --git a/src/vs/server/webClientServer.ts b/src/vs/server/webClientServer.ts new file mode 100644 index 0000000000000..06c0b23c6d992 --- /dev/null +++ b/src/vs/server/webClientServer.ts @@ -0,0 +1,353 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as http from 'http'; +import * as url from 'url'; +import * as util from 'util'; +import * as cookie from 'cookie'; +import * as crypto from 'crypto'; +import { isEqualOrParent, sanitizeFilePath } from 'vs/base/common/extpath'; +import { getMediaMime } from 'vs/base/common/mime'; +import { isLinux } from 'vs/base/common/platform'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; +import { ILogService } from 'vs/platform/log/common/log'; +import product from 'vs/platform/product/common/product'; +import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService'; +import { extname, dirname, join, normalize } from 'vs/base/common/path'; +import { FileAccess } from 'vs/base/common/network'; +import { generateUuid } from 'vs/base/common/uuid'; +import { cwd } from 'vs/base/common/process'; + +const textMimeType = { + '.html': 'text/html', + '.js': 'text/javascript', + '.json': 'application/json', + '.css': 'text/css', + '.svg': 'image/svg+xml', +} as { [ext: string]: string | undefined }; + +/** + * Return an error to the client. + */ +export async function serveError(req: http.IncomingMessage, res: http.ServerResponse, errorCode: number, errorMessage: string): Promise { + res.writeHead(errorCode, { 'Content-Type': 'text/plain' }); + res.end(errorMessage); +} + +/** + * Serve a file at a given path or 404 if the file is missing. + */ +export async function serveFile(logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, filePath: string, responseHeaders: Record = Object.create(null)): Promise { + try { + const stat = await util.promisify(fs.stat)(filePath); + + // Check if file modified since + const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) + if (req.headers['if-none-match'] === etag) { + res.writeHead(304); + return res.end(); + } + + // Headers + responseHeaders['Content-Type'] = textMimeType[extname(filePath)] || getMediaMime(filePath) || 'text/plain'; + responseHeaders['Etag'] = etag; + + res.writeHead(200, responseHeaders); + + // Data + fs.createReadStream(filePath).pipe(res); + } catch (error) { + if (error.code !== 'ENOENT') { + logService.error(error); + console.error(error.toString()); + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + return res.end('Not found'); + } +} + +const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath); + +export class WebClientServer { + + private _mapCallbackUriToRequestId: Map = new Map(); + + constructor( + private readonly _connectionToken: string, + private readonly _environmentService: IServerEnvironmentService, + private readonly _logService: ILogService + ) { } + + async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + try { + const pathname = parsedUrl.pathname!; + + if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname === '/code-192.png' || pathname === '/code-512.png') { + // always serve icons/manifest, even without a token + return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', pathname.substr(1))); + } + if (/^\/static\//.test(pathname)) { + // always serve static requests, even without a token + return this._handleStatic(req, res, parsedUrl); + } + if (pathname === '/') { + // the token handling is done inside the handler + return this._handleRoot(req, res, parsedUrl); + } + if (pathname === '/callback') { + // callback support + return this._handleCallback(req, res, parsedUrl); + } + if (pathname === '/fetch-callback') { + // callback fetch support + return this._handleFetchCallback(req, res, parsedUrl); + } + + return serveError(req, res, 404, 'Not found.'); + } catch (error) { + this._logService.error(error); + console.error(error.toString()); + + return serveError(req, res, 500, 'Internal Server Error.'); + } + } + + private _hasCorrectTokenCookie(req: http.IncomingMessage): boolean { + const cookies = cookie.parse(req.headers.cookie || ''); + return (cookies['vscode-tkn'] === this._connectionToken); + } + + /** + * Handle HTTP requests for /static/* + */ + private async _handleStatic(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + const headers: Record = Object.create(null); + + // Strip `/static/` from the path + const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20) + const relativeFilePath = normalize(normalizedPathname.substr('/static/'.length)); + + const filePath = join(APP_ROOT, relativeFilePath); + if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) { + return serveError(req, res, 400, `Bad request.`); + } + + return serveFile(this._logService, req, res, filePath, headers); + } + + /** + * Handle HTTP requests for / + */ + private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { + if (!req.headers.host) { + return serveError(req, res, 400, `Bad request.`); + } + + const queryTkn = parsedUrl.query['tkn']; + if (typeof queryTkn === 'string') { + // tkn came in via a query string + // => set a cookie and redirect to url without tkn + const responseHeaders: Record = Object.create(null); + responseHeaders['Set-Cookie'] = cookie.serialize('vscode-tkn', queryTkn, { sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 /* 1 week */ }); + + const newQuery = Object.create(null); + for (let key in parsedUrl.query) { + if (key !== 'tkn') { + newQuery[key] = parsedUrl.query[key]; + } + } + const newLocation = url.format({ pathname: '/', query: newQuery }); + responseHeaders['Location'] = newLocation; + + res.writeHead(302, responseHeaders); + return res.end(); + } + + if (this._environmentService.isBuilt && !this._hasCorrectTokenCookie(req)) { + return serveError(req, res, 403, `Forbidden.`); + } + + const remoteAuthority = req.headers.host; + const transformer = createRemoteURITransformer(remoteAuthority); + const { workspacePath, isFolder } = await this._getWorkspaceFromCLI(); + + function escapeAttribute(value: string): string { + return value.replace(/"/g, '"'); + } + + let _wrapWebWorkerExtHostInIframe: undefined | false = undefined; + if (this._environmentService.driverHandle) { + // integration tests run at a time when the built output is not yet published to the CDN + // so we must disable the iframe wrapping because the iframe URL will give a 404 + _wrapWebWorkerExtHostInIframe = false; + } + + const filePath = FileAccess.asFileUri(this._environmentService.isBuilt ? 'vs/code/browser/workbench/workbench.html' : 'vs/code/browser/workbench/workbench-dev.html', require).fsPath; + const authSessionInfo = !this._environmentService.isBuilt && this._environmentService.args['github-auth'] ? { + id: generateUuid(), + providerId: 'github', + accessToken: this._environmentService.args['github-auth'], + scopes: [['user:email'], ['repo']] + } : undefined; + const data = (await util.promisify(fs.readFile)(filePath)).toString() + .replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({ + folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, + workspaceUri: (workspacePath && !isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, + remoteAuthority, + _wrapWebWorkerExtHostInIframe, + developmentOptions: { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined }, + settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, + }))) + .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : ''); + + const cspDirectives = [ + 'default-src \'self\';', + 'img-src \'self\' https: data: blob:;', + 'media-src \'none\';', + `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-cb2sg39EJV8ABaSNFfWu/ou8o1xVXYK7jp90oZ9vpcg=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html + 'child-src \'self\';', + `frame-src 'self' https://*.vscode-webview.net ${product.webEndpointUrl || ''} data:;`, + 'worker-src \'self\' data:;', + 'style-src \'self\' \'unsafe-inline\';', + 'connect-src \'self\' ws: wss: https:;', + 'font-src \'self\' blob:;', + 'manifest-src \'self\';' + ].join(' '); + + res.writeHead(200, { + 'Content-Type': 'text/html', + // At this point we know the client has a valid cookie + // and we want to set it prolong it to ensure that this + // client is valid for another 1 week at least + 'Set-Cookie': cookie.serialize('vscode-tkn', this._connectionToken, { sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 /* 1 week */ }), + 'Content-Security-Policy': cspDirectives + }); + return res.end(data); + } + + private _getScriptCspHashes(content: string): string[] { + // Compute the CSP hashes for line scripts. Uses regex + // which means it isn't 100% good. + const regex = /