Add server folder

This commit is contained in:
Alex Dima 2021-10-20 18:42:13 +02:00
parent fc504f3af3
commit 822f995357
No known key found for this signature in database
GPG Key ID: 39563C1504FDD0C9
46 changed files with 5165 additions and 25 deletions

2
.gitignore vendored
View File

@ -7,8 +7,6 @@ node_modules/
extensions/**/dist/
/out*/
/extensions/**/out/
src/vs/server
resources/server
build/node_modules
coverage/
test_data/

View File

@ -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) {

View File

@ -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);
});

View File

@ -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());
});
}

View File

@ -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

View File

@ -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" "$@"

View File

@ -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

View File

@ -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" "$@"

View File

@ -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

View File

@ -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 "$@"

View File

@ -0,0 +1,4 @@
@echo off
setlocal
call "%~dp0..\node" "%~dp0..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" %*
endlocal

View File

@ -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" "$@"

View File

@ -0,0 +1,4 @@
@echo off
setlocal
call "%~dp0..\..\node" "%~dp0..\..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" "--openExternal" %*
endlocal

View File

@ -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" "$@"

View File

@ -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

View File

@ -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" "$@"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -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"
}
]
}

View File

@ -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

View File

@ -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

View File

@ -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%

View File

@ -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 "$@"

View File

@ -0,0 +1,6 @@
@echo off
setlocal
node %~dp0\bin-dev\code-web.js --selfhost %*
endlocal

View File

@ -0,0 +1,2 @@
#!/usr/bin/env sh
node $(dirname "$0")/bin-dev/code-web.js --selfhost "$@"

24
resources/server/web.bat Normal file
View File

@ -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

26
resources/server/web.sh Executable file
View File

@ -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 "$@"

17
src/vs/server/cli.js Normal file
View File

@ -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');

View File

@ -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<IProcessEnvironment> {
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<void>,
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: (<Buffer>this.initialDataChunk.buffer).toString('base64'),
skipWebSocketFrames: this.skipWebSocketFrames,
permessageDeflate: this.permessageDeflate,
inflateBytes: (<Buffer>this.inflateBytes.buffer).toString('base64'),
};
}
}
export class ExtensionHostConnection {
private _onClose = new Emitter<void>();
readonly onClose: Event<void> = 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<void> {
// 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<void> {
try {
let execArgv: string[] = [];
if (startParams.port && !(<any>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<string>(this._extensionHostProcess.stdout!, 'data');
const onStderr = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stderr!, 'data');
onStdout((e) => this._log(`<${pid}> ${e}`));
onStderr((e) => this._log(`<${pid}><stderr> ${e}`));
// Support logging from extension host
this._extensionHostProcess.on('message', msg => {
if (msg && (<IRemoteConsoleLog>msg).type === '__$console') {
logRemoteEntry(this._logService, (<IRemoteConsoleLog>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];
}
}
}

156
src/vs/server/main.js Normal file
View File

@ -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<IServerAPI> | null} */
let _remoteExtensionHostAgentServerPromise = null;
/** @returns {Promise<IServerAPI>} */
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<typeof import('./remoteExtensionHostAgent')> } */
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();

View File

@ -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<void>;
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<any> {
switch (command) {
case 'disableTelemetry': {
this.telemetryService.permanentlyDisableTelemetry();
return;
}
case 'getEnvironmentData': {
const args = <IGetEnvironmentDataArguments>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 = <IScanExtensionsArguments>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 = <IScanSingleExtensionArguments>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 = <IDiagnosticInfoOptions>arg;
const diagnosticInfo: IDiagnosticInfo = {
machineInfo: getMachineInfo()
};
const processesPromise: Promise<ProcessItem | void> = options.includeProcesses ? listProcesses(process.pid) : Promise.resolve();
let workspaceMetadataPromises: Promise<void>[] = [];
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<any> {
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(<LocWhenUser>extension.contributes.menus);
}
if (extension.contributes.keybindings) {
_massageWhenUserArr(<WhenUser | WhenUser[]>extension.contributes.keybindings);
}
if (extension.contributes.views) {
_massageLocWhenUser(<LocWhenUser>extension.contributes.views);
}
}
});
}
private async _getEnvironmentData(): Promise<IRemoteAgentEnvironmentDTO> {
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<Translations> {
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<IExtensionDescription[]> {
// 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<string, IExtensionDescription>();
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<IExtensionDescription[]> {
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<IExtensionDescription[]> {
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<IExtensionDescription[]> = builtinExtensions;
if (devMode) {
class ExtraBuiltInExtensionResolver implements IExtensionResolver {
constructor(private builtInExtensions: IBuiltInExtension[]) { }
resolveExtensions(): Promise<IExtensionReference[]> {
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<IExtensionDescription[]> {
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<IExtensionDescription | null> {
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);
}
}

View File

@ -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<number, IDisposable>();
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<IFileChange[] | string>
) {
super();
this.registerListeners(emitter);
}
private registerListeners(emitter: Emitter<IFileChange[] | string>): void {
const localChangeEmitter = this._register(new Emitter<readonly IFileChange[]>());
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<RemoteAgentConnectionContext> {
private readonly BUFFER_SIZE = 256 * 1024; // slightly larger to reduce remote-communication overhead
private readonly uriTransformerCache = new Map<string, IURITransformer>();
private readonly fileWatchers = new Map<string, SessionFileWatcher>();
private readonly fsProvider = this._register(new DiskFileSystemProvider(this.logService, { bufferSize: this.BUFFER_SIZE }));
private readonly watchRequests = new Map<string, IDisposable>();
constructor(
private readonly logService: ILogService,
private readonly environmentService: IServerEnvironmentService
) {
super();
}
call(ctx: RemoteAgentConnectionContext, command: string, arg?: any): Promise<any> {
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<any> {
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<IFileChange[] | string> {
const emitter = new Emitter<IFileChange[] | string>({
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<ReadableStreamEventPayload<VSBuffer>> {
const resource = this.transformIncoming(uriTransformer, _resource, true);
const cancellableSource = new CancellationTokenSource();
const emitter = new Emitter<ReadableStreamEventPayload<VSBuffer>>({
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<IStat> {
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<number> {
const resource = this.transformIncoming(uriTransformer, _resource, true);
return this.fsProvider.open(resource, opts);
}
private close(_fd: number): Promise<void> {
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<VSBuffer> {
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<number> {
return this.fsProvider.write(fd, pos, data.buffer, offset, length);
}
private writeFile(uriTransformer: IURITransformer, _resource: UriComponents, content: VSBuffer, opts: FileWriteOptions): Promise<void> {
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<void> {
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<void> {
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<void> {
const resource = this.transformIncoming(uriTransformer, _resource);
return this.fsProvider.mkdir(resource);
}
private delete(uriTransformer: IURITransformer, _resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
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();
}
}

400
src/vs/server/remoteCli.ts Normal file
View File

@ -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<RemoteParsedArgs> = { ...OPTIONS };
const isSupported = cliCommand ? isSupportedForCmd : isSupportedForPipe;
for (const optionId in OPTIONS) {
const optId = <keyof RemoteParsedArgs>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[<keyof NativeParsedArgs>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<any> {
return new Promise<string>(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);

View File

@ -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');
(<any>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<IServerAPI> {
return doCreateServer(address, args, REMOTE_DATA_FOLDER);
}

View File

@ -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<void> {
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<IInstantiationService> {
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<void> {
// 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<void> {
const cliMain = new CliMain(args, REMOTE_DATA_FOLDER);
try {
await cliMain.run();
eventuallyExit(0);
} catch (err) {
eventuallyExit(1);
} finally {
cliMain.dispose();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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));

View File

@ -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<void>();
public readonly onClose: Event<void> = 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();
}
}

View File

@ -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<string, Promise<lp.NLSConfiguration>> = new Map();
function exists(file: string) {
return new Promise(c => fs.exists(file, c));
}
export function getNLSConfiguration(language: string, userDataPath: string): Promise<lp.NLSConfiguration> {
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';
}
}

View File

@ -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<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLog(eventName, data, anonymizeFilePaths);
}
override publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>, anonymizeFilePaths?: boolean): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLog2(eventName, data, anonymizeFilePaths);
}
override publicLogError(errorEventName: string, data?: ITelemetryData): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLogError(errorEventName, data);
}
override publicLogError2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): Promise<void> {
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, IRemoteTelemetryService>(ITelemetryService);

View File

@ -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<RemoteAgentConnectionContext> {
private _lastReqId = 0;
private readonly _pendingCommands = new Map<number, {
resolve: (data: any) => 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<any> {
switch (command) {
case '$restartPtyHost': return this._ptyService.restartPtyHost?.apply(this._ptyService, args);
case '$createProcess': {
const uriTransformer = createRemoteURITransformer(ctx.remoteAuthority);
return this._createProcess(uriTransformer, <ICreateTerminalProcessArguments>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(<IGetTerminalLayoutInfoArgs>args);
case '$setTerminalLayoutInfo': return this._setTerminalLayoutInfo(<ISetTerminalLayoutInfoArgs>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<any> {
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<ICreateTerminalProcessResult> {
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<string, IEnvironmentVariableCollection>(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: <T>(id: string, ...args: any[]): Promise<T> => 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<T>(commandId: string, commandArgs: any[], uriTransformer: IURITransformer): Promise<T> {
let resolve!: (data: any) => void;
let reject!: (err: any) => void;
const result = new Promise<T>((_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<string> {
return this._ptyService.getDefaultSystemShell(osOverride);
}
private async _getProfiles(workspaceId: string, profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]> {
return this._ptyService.getProfiles?.(workspaceId, profiles, defaultProfile, includeDetectedProfiles) || [];
}
private _getEnvironment(): platform.IProcessEnvironment {
return { ...process.env };
}
private _getWslPath(original: string): Promise<string> {
return this._ptyService.getWslPath(original);
}
private _setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void {
this._ptyService.setTerminalLayoutInfo(args);
}
private async _getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined> {
return this._ptyService.getTerminalLayoutInfo(args);
}
private _reduceConnectionGraceTime(): Promise<void> {
return this._ptyService.reduceConnectionGraceTime();
}
}

View File

@ -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 = <any>require.__$__nodeRequire(uriTransformerPath);
const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
return new URITransformer(rawURITransformer);
}

View File

@ -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<ServerParsedArgs> = {
'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, IServerEnvironmentService>(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; }
}

View File

@ -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;
}
};
};

View File

@ -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<void> {
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<string, string> = Object.create(null)): Promise<void> {
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<string, UriComponents> = 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<void> {
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<void> {
const headers: Record<string, string> = 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<void> {
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<string, string> = 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, '&quot;');
}
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 = /<script>([\s\S]+?)<\/script>/img;
const result: string[] = [];
let match: RegExpExecArray | null;
while (match = regex.exec(content)) {
const hasher = crypto.createHash('sha256');
// This only works on Windows if we strip `\r` from `\r\n`.
const script = match[1].replace(/\r\n/g, '\n');
const hash = hasher
.update(Buffer.from(script))
.digest().toString('base64');
result.push(`'sha256-${hash}'`);
}
return result;
}
private async _getWorkspaceFromCLI(): Promise<{ workspacePath?: string, isFolder?: boolean }> {
// check for workspace argument
const workspaceCandidate = this._environmentService.args['workspace'];
if (workspaceCandidate && workspaceCandidate.length > 0) {
const workspace = sanitizeFilePath(workspaceCandidate, cwd());
if (await util.promisify(fs.exists)(workspace)) {
return { workspacePath: workspace };
}
}
// check for folder argument
const folderCandidate = this._environmentService.args['folder'];
if (folderCandidate && folderCandidate.length > 0) {
const folder = sanitizeFilePath(folderCandidate, cwd());
if (await util.promisify(fs.exists)(folder)) {
return { workspacePath: folder, isFolder: true };
}
}
// empty window otherwise
return {};
}
private _getFirstQueryValue(parsedUrl: url.UrlWithParsedQuery, key: string): string | undefined {
const result = parsedUrl.query[key];
return Array.isArray(result) ? result[0] : result;
}
private _getFirstQueryValues(parsedUrl: url.UrlWithParsedQuery, ignoreKeys?: string[]): Map<string, string> {
const queryValues = new Map<string, string>();
for (const key in parsedUrl.query) {
if (ignoreKeys && ignoreKeys.indexOf(key) >= 0) {
continue;
}
const value = this._getFirstQueryValue(parsedUrl, key);
if (typeof value === 'string') {
queryValues.set(key, value);
}
}
return queryValues;
}
/**
* Handle HTTP requests for /callback
*/
private async _handleCallback(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
const wellKnownKeys = ['vscode-requestId', 'vscode-scheme', 'vscode-authority', 'vscode-path', 'vscode-query', 'vscode-fragment'];
const [requestId, vscodeScheme, vscodeAuthority, vscodePath, vscodeQuery, vscodeFragment] = wellKnownKeys.map(key => {
const value = this._getFirstQueryValue(parsedUrl, key);
if (value) {
return decodeURIComponent(value);
}
return value;
});
if (!requestId) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
return res.end(`Bad request.`);
}
// merge over additional query values that we got
let query: string | undefined = vscodeQuery;
let index = 0;
this._getFirstQueryValues(parsedUrl, wellKnownKeys).forEach((value, key) => {
if (!query) {
query = '';
}
const prefix = (index++ === 0) ? '' : '&';
query += `${prefix}${key}=${value}`;
});
// add to map of known callbacks
this._mapCallbackUriToRequestId.set(requestId, URI.from({ scheme: vscodeScheme || product.urlProtocol, authority: vscodeAuthority, path: vscodePath, query, fragment: vscodeFragment }).toJSON());
return serveFile(this._logService, req, res, FileAccess.asFileUri('vs/code/browser/workbench/callback.html', require).fsPath, { 'Content-Type': 'text/html' });
}
/**
* Handle HTTP requests for /fetch-callback
*/
private async _handleFetchCallback(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
const requestId = this._getFirstQueryValue(parsedUrl, 'vscode-requestId')!;
if (!requestId) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
return res.end(`Bad request.`);
}
const knownCallbackUri = this._mapCallbackUriToRequestId.get(requestId);
if (knownCallbackUri) {
this._mapCallbackUriToRequestId.delete(requestId);
}
res.writeHead(200, { 'Content-Type': 'text/json' });
return res.end(JSON.stringify(knownCallbackUri));
}
}