Add server folder
This commit is contained in:
parent
fc504f3af3
commit
822f995357
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,8 +7,6 @@ node_modules/
|
|||
extensions/**/dist/
|
||||
/out*/
|
||||
/extensions/**/out/
|
||||
src/vs/server
|
||||
resources/server
|
||||
build/node_modules
|
||||
coverage/
|
||||
test_data/
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
87
resources/server/bin-dev/code-web.js
Normal file
87
resources/server/bin-dev/code-web.js
Normal 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());
|
||||
});
|
||||
}
|
6
resources/server/bin-dev/code.cmd
Normal file
6
resources/server/bin-dev/code.cmd
Normal 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
|
18
resources/server/bin-dev/code.sh
Executable file
18
resources/server/bin-dev/code.sh
Executable 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" "$@"
|
6
resources/server/bin-dev/helpers/browser.cmd
Normal file
6
resources/server/bin-dev/helpers/browser.cmd
Normal 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
|
18
resources/server/bin-dev/helpers/browser.sh
Executable file
18
resources/server/bin-dev/helpers/browser.sh
Executable 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" "$@"
|
43
resources/server/bin-dev/server.bat
Normal file
43
resources/server/bin-dev/server.bat
Normal 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
|
28
resources/server/bin-dev/server.sh
Executable file
28
resources/server/bin-dev/server.sh
Executable 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 "$@"
|
4
resources/server/bin/code.cmd
Normal file
4
resources/server/bin/code.cmd
Normal file
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
setlocal
|
||||
call "%~dp0..\node" "%~dp0..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" %*
|
||||
endlocal
|
12
resources/server/bin/code.sh
Normal file
12
resources/server/bin/code.sh
Normal 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" "$@"
|
4
resources/server/bin/helpers/browser.cmd
Normal file
4
resources/server/bin/helpers/browser.cmd
Normal file
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
setlocal
|
||||
call "%~dp0..\..\node" "%~dp0..\..\out\vs\server\cli.js" "@@APPNAME@@" "@@VERSION@@" "@@COMMIT@@" "@@APPNAME@@.cmd" "--openExternal" %*
|
||||
endlocal
|
12
resources/server/bin/helpers/browser.sh
Normal file
12
resources/server/bin/helpers/browser.sh
Normal 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" "$@"
|
24
resources/server/bin/server.cmd
Normal file
24
resources/server/bin/server.cmd
Normal 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
|
12
resources/server/bin/server.sh
Normal file
12
resources/server/bin/server.sh
Normal 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" "$@"
|
BIN
resources/server/code-192.png
Normal file
BIN
resources/server/code-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
resources/server/code-512.png
Normal file
BIN
resources/server/code-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
resources/server/favicon.ico
Normal file
BIN
resources/server/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
19
resources/server/manifest.json
Normal file
19
resources/server/manifest.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
79
resources/server/test/test-remote-integration.bat
Normal file
79
resources/server/test/test-remote-integration.bat
Normal 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
|
114
resources/server/test/test-remote-integration.sh
Executable file
114
resources/server/test/test-remote-integration.sh
Executable 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
|
55
resources/server/test/test-web-integration.bat
Normal file
55
resources/server/test/test-web-integration.bat
Normal 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%
|
36
resources/server/test/test-web-integration.sh
Executable file
36
resources/server/test/test-web-integration.sh
Executable 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 "$@"
|
6
resources/server/web-selfhost.bat
Normal file
6
resources/server/web-selfhost.bat
Normal file
|
@ -0,0 +1,6 @@
|
|||
@echo off
|
||||
setlocal
|
||||
|
||||
node %~dp0\bin-dev\code-web.js --selfhost %*
|
||||
|
||||
endlocal
|
2
resources/server/web-selfhost.sh
Executable file
2
resources/server/web-selfhost.sh
Executable 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
24
resources/server/web.bat
Normal 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
26
resources/server/web.sh
Executable 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
17
src/vs/server/cli.js
Normal 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');
|
272
src/vs/server/extensionHostConnection.ts
Normal file
272
src/vs/server/extensionHostConnection.ts
Normal 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
156
src/vs/server/main.js
Normal 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();
|
507
src/vs/server/remoteAgentEnvironmentImpl.ts
Normal file
507
src/vs/server/remoteAgentEnvironmentImpl.ts
Normal 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);
|
||||
}
|
||||
}
|
305
src/vs/server/remoteAgentFileSystemImpl.ts
Normal file
305
src/vs/server/remoteAgentFileSystemImpl.ts
Normal 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
400
src/vs/server/remoteCli.ts
Normal 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);
|
||||
|
64
src/vs/server/remoteExtensionHostAgent.ts
Normal file
64
src/vs/server/remoteExtensionHostAgent.ts
Normal 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);
|
||||
}
|
145
src/vs/server/remoteExtensionHostAgentCli.ts
Normal file
145
src/vs/server/remoteExtensionHostAgentCli.ts
Normal 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();
|
||||
}
|
||||
}
|
1039
src/vs/server/remoteExtensionHostAgentServer.ts
Normal file
1039
src/vs/server/remoteExtensionHostAgentServer.ts
Normal file
File diff suppressed because it is too large
Load diff
8
src/vs/server/remoteExtensionHostProcess.ts
Normal file
8
src/vs/server/remoteExtensionHostProcess.ts
Normal 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));
|
127
src/vs/server/remoteExtensionManagement.ts
Normal file
127
src/vs/server/remoteExtensionManagement.ts
Normal 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();
|
||||
}
|
||||
}
|
46
src/vs/server/remoteLanguagePacks.ts
Normal file
46
src/vs/server/remoteLanguagePacks.ts
Normal 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';
|
||||
}
|
||||
}
|
64
src/vs/server/remoteTelemetryService.ts
Normal file
64
src/vs/server/remoteTelemetryService.ts
Normal 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);
|
332
src/vs/server/remoteTerminalChannel.ts
Normal file
332
src/vs/server/remoteTerminalChannel.ts
Normal 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();
|
||||
}
|
||||
}
|
15
src/vs/server/remoteUriTransformer.ts
Normal file
15
src/vs/server/remoteUriTransformer.ts
Normal 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);
|
||||
}
|
126
src/vs/server/serverEnvironmentService.ts
Normal file
126
src/vs/server/serverEnvironmentService.ts
Normal 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; }
|
||||
}
|
49
src/vs/server/uriTransformer.js
Normal file
49
src/vs/server/uriTransformer.js
Normal 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;
|
||||
}
|
||||
};
|
||||
};
|
353
src/vs/server/webClientServer.ts
Normal file
353
src/vs/server/webClientServer.ts
Normal 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, '"');
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue