Merge vscode 1.67 (#20883)

* Fix initial build breaks from 1.67 merge (#2514)

* Update yarn lock files

* Update build scripts

* Fix tsconfig

* Build breaks

* WIP

* Update yarn lock files

* Misc breaks

* Updates to package.json

* Breaks

* Update yarn

* Fix breaks

* Breaks

* Build breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Missing file

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Fix several runtime breaks (#2515)

* Missing files

* Runtime breaks

* Fix proxy ordering issue

* Remove commented code

* Fix breaks with opening query editor

* Fix post merge break

* Updates related to setup build and other breaks (#2516)

* Fix bundle build issues

* Update distro

* Fix distro merge and update build JS files

* Disable pipeline steps

* Remove stats call

* Update license name

* Make new RPM dependencies a warning

* Fix extension manager version checks

* Update JS file

* Fix a few runtime breaks

* Fixes

* Fix runtime issues

* Fix build breaks

* Update notebook tests (part 1)

* Fix broken tests

* Linting errors

* Fix hygiene

* Disable lint rules

* Bump distro

* Turn off smoke tests

* Disable integration tests

* Remove failing "activate" test

* Remove failed test assertion

* Disable other broken test

* Disable query history tests

* Disable extension unit tests

* Disable failing tasks
This commit is contained in:
Karl Burtram
2022-10-19 19:13:18 -07:00
committed by GitHub
parent 33c6daaea1
commit 8a3d08f0de
3738 changed files with 192313 additions and 107208 deletions

View File

@@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
const path = require('path');
// Keep bootstrap-amd.js from redefining 'fs'.
delete process.env['ELECTRON_RUN_AS_NODE'];
// Set default remote native node modules path, if unset
process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', '..', '..', 'remote', 'node_modules');
require('../../bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']);
require('../../bootstrap-amd').load('vs/server/remoteCli');

View File

@@ -1,158 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
const perf = require('../base/common/performance');
const performance = require('perf_hooks').performance;
const product = require('../../../product.json');
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 () => {
const serverGreeting = product.serverGreeting.join('\n');
let output = serverGreeting ? `\n\n${serverGreeting}\n\n` : ``;
if (typeof nodeListenOptions.port === 'number' && parsedArgs['print-ip-address']) {
const ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach(function (ifname) {
ifaces[ifname].forEach(function (iface) {
if (!iface.internal && iface.family === 'IPv4') {
output += `IP Address: ${iface.address}\n`;
}
});
});
}
address = server.address();
if (address === null) {
throw new Error('Unexpected server address');
}
// Do not change this line. VS Code looks for this in the output.
output += `Extension host agent listening on ${typeof address === 'string' ? address : address.port}\n`;
console.log(output);
perf.mark('code/server/started');
// @ts-ignore
global.vscodeServerListenTime = performance.now();
await getRemoteExtensionHostAgentServer();
});
process.on('exit', () => {
server.close();
if (_remoteExtensionHostAgentServer) {
_remoteExtensionHostAgentServer.dispose();
}
});
}
/**
* @param {string | undefined} strPort
* @returns {number}
*/
function parsePort(strPort) {
try {
if (strPort) {
return parseInt(strPort);
}
} catch (e) {
console.log('Port is not a number, using 8000 instead.');
}
return 8000;
}
/** @returns { Promise<typeof import('./remoteExtensionHostAgent')> } */
function loadCode() {
return new Promise((resolve, reject) => {
const path = require('path');
// Set default remote native node modules path, if unset
process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] = process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH'] || path.join(__dirname, '..', '..', '..', 'remote', 'node_modules');
require('../../bootstrap-node').injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']);
require('../../bootstrap-amd').load('vs/server/remoteExtensionHostAgent', resolve, reject);
});
}
start();

View File

@@ -5,49 +5,43 @@
import * as cp from 'child_process';
import * as net from 'net';
import { getNLSConfiguration } from 'vs/server/remoteLanguagePacks';
import { uriTransformerPath } from 'vs/server/remoteUriTransformer';
import { getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks';
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 { getResolvedShellEnv } from 'vs/platform/shell/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 { IServerEnvironmentService } from 'vs/server/node/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';
import { removeDangerousEnvVariables } from 'vs/base/common/processes';
import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService';
export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, language: string, isDebug: boolean, environmentService: IServerEnvironmentService, logService: ILogService): Promise<IProcessEnvironment> {
export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, withUserShellEnvironment: boolean, 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 = {};
let userShellEnv: typeof process.env = {};
if (withUserShellEnvironment) {
try {
userShellEnv = await getResolvedShellEnv(logService, environmentService.args, process.env);
} catch (error) {
logService.error('ExtensionHostConnection#buildUserEnvironment resolving shell environment failed', error);
}
}
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_AMD_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess',
VSCODE_PIPE_LOGGING: 'true',
VSCODE_VERBOSE_LOGGING: 'true',
VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true',
@@ -57,13 +51,23 @@ export async function buildUserEnvironment(startParamsEnv: { [key: string]: stri
},
...startParamsEnv
};
const binFolder = environmentService.isBuilt ? join(environmentService.appRoot, 'bin') : join(environmentService.appRoot, 'resources', 'server', 'bin-dev');
const remoteCliBinFolder = join(binFolder, 'remote-cli'); // contains the `code` command that can talk to the remote server
let PATH = readCaseInsensitive(env, 'PATH');
if (PATH) {
PATH = remoteCliBinFolder + delimiter + PATH;
} else {
PATH = remoteCliBinFolder;
}
setCaseInsensitive(env, 'PATH', PATH);
if (!environmentService.args['without-browser-env-var']) {
env.BROWSER = join(binFolder, 'helpers', isWindows ? 'browser.cmd' : 'browser.sh');
env.BROWSER = join(binFolder, 'helpers', isWindows ? 'browser.cmd' : 'browser.sh'); // a command that opens a browser on the local machine
}
setCaseInsensitive(env, 'PATH', PATH);
removeNulls(env);
return env;
}
@@ -99,18 +103,18 @@ export class ExtensionHostConnection {
private _connectionData: ConnectionData | null;
constructor(
private readonly _environmentService: IServerEnvironmentService,
private readonly _logService: ILogService,
private readonly _reconnectionToken: string,
remoteAddress: string,
socket: NodeSocket | WebSocketNodeSocket,
initialDataChunk: VSBuffer
initialDataChunk: VSBuffer,
@IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService,
@ILogService private readonly _logService: ILogService,
@IExtensionHostStatusService private readonly _extensionHostStatusService: IExtensionHostStatusService,
) {
this._disposed = false;
this._remoteAddress = remoteAddress;
this._extensionHostProcess = null;
this._connectionData = ExtensionHostConnection._toConnectionData(socket, initialDataChunk);
this._connectionData.socket.pause();
this._log(`New connection established.`);
}
@@ -156,7 +160,6 @@ export class ExtensionHostConnection {
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
@@ -188,10 +191,10 @@ export class ExtensionHostConnection {
try {
let execArgv: string[] = [];
if (startParams.port && !(<any>process).pkg) {
execArgv = [`--inspect${startParams.break ? '-brk' : ''}=0.0.0.0:${startParams.port}`];
execArgv = [`--inspect${startParams.break ? '-brk' : ''}=${startParams.port}`];
}
const env = await buildUserEnvironment(startParams.env, startParams.language, !!startParams.debugId, this._environmentService, this._logService);
const env = await buildUserEnvironment(startParams.env, true, startParams.language, !!startParams.debugId, this._environmentService, this._logService);
removeDangerousEnvVariables(env);
const opts = {
@@ -201,11 +204,9 @@ export class ExtensionHostConnection {
};
// Run Extension Host as fork of current process
const args = ['--type=extensionHost', `--uriTransformerPath=${uriTransformerPath}`];
const args = ['--type=extensionHost', `--transformURIs`];
const useHostProxy = this._environmentService.args['use-host-proxy'];
if (useHostProxy !== undefined) {
args.push(`--useHostProxy=${useHostProxy}`);
}
args.push(`--useHostProxy=${useHostProxy ? 'true' : 'false'}`);
this._extensionHostProcess = cp.fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, args, opts);
const pid = this._extensionHostProcess.pid;
this._log(`<${pid}> Launched Extension Host Process.`);
@@ -234,6 +235,7 @@ export class ExtensionHostConnection {
});
this._extensionHostProcess.on('exit', (code: number, signal: string) => {
this._extensionHostStatusService.setExitInfo(this._reconnectionToken, { code, signal });
this._log(`<${pid}> Extension Host Process exited with code: ${code}, signal: ${signal}.`);
this._cleanResources();
});
@@ -256,6 +258,12 @@ export class ExtensionHostConnection {
}
}
function readCaseInsensitive(env: { [key: string]: string | undefined }, key: string): string | undefined {
const pathKeys = Object.keys(env).filter(k => k.toLowerCase() === key.toLowerCase());
const pathKey = pathKeys.length > 0 ? pathKeys[0] : key;
return env[pathKey];
}
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;

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionHostExitInfo } from 'vs/workbench/services/remote/common/remoteAgentService';
export const IExtensionHostStatusService = createDecorator<IExtensionHostStatusService>('extensionHostStatusService');
export interface IExtensionHostStatusService {
readonly _serviceBrand: undefined;
setExitInfo(reconnectionToken: string, info: IExtensionHostExitInfo): void;
getExitInfo(reconnectionToken: string): IExtensionHostExitInfo | null;
}
export class ExtensionHostStatusService implements IExtensionHostStatusService {
_serviceBrand: undefined;
private readonly _exitInfo = new Map<string, IExtensionHostExitInfo>();
setExitInfo(reconnectionToken: string, info: IExtensionHostExitInfo): void {
this._exitInfo.set(reconnectionToken, info);
}
getExitInfo(reconnectionToken: string): IExtensionHostExitInfo | null {
return this._exitInfo.get(reconnectionToken) || null;
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { AbstractExtensionsScannerService, IExtensionsScannerService, Translations } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { getNLSConfiguration, InternalNLSConfiguration } from 'vs/server/node/remoteLanguagePacks';
export class ExtensionsScannerService extends AbstractExtensionsScannerService implements IExtensionsScannerService {
constructor(
@IFileService fileService: IFileService,
@ILogService logService: ILogService,
@INativeEnvironmentService private readonly nativeEnvironmentService: INativeEnvironmentService,
@IProductService productService: IProductService,
) {
super(
URI.file(nativeEnvironmentService.builtinExtensionsPath),
URI.file(nativeEnvironmentService.extensionsPath),
joinPath(nativeEnvironmentService.userHome, '.vscode-oss-dev', 'extensions', 'control.json'),
joinPath(URI.file(nativeEnvironmentService.userDataPath), MANIFEST_CACHE_FOLDER),
fileService, logService, nativeEnvironmentService, productService);
}
protected async getTranslations(language: string): Promise<Translations> {
const config = await getNLSConfiguration(language, this.nativeEnvironmentService.userDataPath);
if (InternalNLSConfiguration.is(config)) {
try {
const content = await this.fileService.readFile(URI.file(config._translationsConfigFile));
return JSON.parse(content.value.toString());
} catch (err) { /* Ignore error */ }
}
return Object.create(null);
}
}

View File

@@ -7,105 +7,68 @@ 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 { createURITransformer } from 'vs/workbench/api/node/uriTransformer';
import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IScanExtensionsArguments, IScanSingleExtensionArguments, IGetExtensionHostExitInfoArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel';
import * as nls from 'vs/nls';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService';
import { ExtensionScanner, ExtensionScannerInput, IExtensionResolver, IExtensionReference } from 'vs/workbench/services/extensions/node/extensionPoints';
import { Schemas } from 'vs/base/common/network';
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionIdentifier, ExtensionType, 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 { basename, isAbsolute, join, resolve } 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 { IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { cwd } from 'vs/base/common/process';
import { IRemoteTelemetryService } from 'vs/server/remoteTelemetryService';
import { Promises } from 'vs/base/node/pfs';
import { IProductService } from 'vs/platform/product/common/productService';
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;
}
import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken';
import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService';
import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService';
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,
private readonly _connectionToken: ServerConnectionToken,
private readonly _environmentService: IServerEnvironmentService,
extensionManagementCLIService: IExtensionManagementCLIService,
private readonly logService: ILogService,
private readonly telemetryService: IRemoteTelemetryService,
private readonly telemetryAppender: ITelemetryAppender | null,
private readonly productService: IProductService
private readonly _logService: ILogService,
private readonly _extensionHostStatusService: IExtensionHostStatusService,
private readonly _extensionsScannerService: IExtensionsScannerService,
) {
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'])
if (_environmentService.args['install-builtin-extension']) {
const installOptions: InstallOptions = { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] };
this.whenExtensionsReady = extensionManagementCLIService.installExtensions([], _environmentService.args['install-builtin-extension'], installOptions, !!_environmentService.args['force'])
.then(null, error => {
logService.error(error);
_logService.error(error);
});
} else {
this.whenExtensionsReady = Promise.resolve();
}
const extensionsToInstall = environmentService.args['install-extension'];
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(() => extensionManagementCLIService.installExtensions(idsOrVSIX, [], { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }, !!_environmentService.args['force']))
.then(null, error => {
logService.error(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);
const uriTransformer = createURITransformer(args.remoteAuthority);
let environmentData = await this._getEnvironmentData();
environmentData = transformOutgoingURIs(environmentData, uriTransformer);
@@ -113,6 +76,11 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
return environmentData;
}
case 'getExtensionHostExitInfo': {
const args = <IGetExtensionHostExitInfoArguments>arg;
return this._extensionHostStatusService.getExitInfo(args.reconnectionToken);
}
case 'whenExtensionsReady': {
await this.whenExtensionsReady;
return;
@@ -122,8 +90,8 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
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);
this._logService.trace(`Scanning extensions using UI language: ${language}`);
const uriTransformer = createURITransformer(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;
@@ -131,7 +99,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
let extensions = await this._scanExtensions(language, extensionDevelopmentPath);
extensions = transformOutgoingURIs(extensions, uriTransformer);
this.logService.trace('Scanned Extensions', extensions);
this._logService.trace('Scanned Extensions', extensions);
RemoteAgentEnvironmentChannel._massageWhenConditions(extensions);
return extensions;
@@ -142,7 +110,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
const args = <IScanSingleExtensionArguments>arg;
const language = args.language;
const isBuiltin = args.isBuiltin;
const uriTransformer = createRemoteURITransformer(args.remoteAuthority);
const uriTransformer = createURITransformer(args.remoteAuthority);
const extensionLocation = URI.revive(uriTransformer.transformIncoming(args.extensionLocation));
const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null;
@@ -150,8 +118,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
return null;
}
const translations = await this._getTranslations(language);
let extension = await this._scanSingleExtension(extensionPath, isBuiltin, language, translations);
let extension = await this._scanSingleExtension(extensionPath, isBuiltin, language);
if (!extension) {
return null;
@@ -176,7 +143,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
const workspaceMetadata: { [key: string]: any } = {};
if (options.folders) {
// only incoming paths are transformed, so remote authority is unneeded.
const uriTransformer = createRemoteURITransformer('');
const uriTransformer = createURITransformer('');
const folderPaths = options.folders
.map(folder => URI.revive(uriTransformer.transformIncoming(folder)))
.filter(uri => uri.scheme === 'file');
@@ -195,26 +162,6 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
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`);
@@ -227,9 +174,9 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
private static _massageWhenConditions(extensions: IExtensionDescription[]): void {
// Massage "when" conditions which mention `resourceScheme`
interface WhenUser { when?: string; }
interface WhenUser { when?: string }
interface LocWhenUser { [loc: string]: WhenUser[]; }
interface LocWhenUser { [loc: string]: WhenUser[] }
const _mapResourceSchemeValue = (value: string, isRegex: boolean): string => {
// console.log(`_mapResourceSchemeValue: ${value}, ${isRegex}`);
@@ -337,44 +284,30 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
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,
connectionToken: (this._connectionToken.type !== ServerConnectionTokenType.None ? this._connectionToken.value : ''),
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,
localHistoryHome: this._environmentService.localHistoryHome,
userHome: this._environmentService.userHome,
os: platform.OS,
arch: process.arch,
marks: performance.getMarks(),
useHostProxy: (this.environmentService.args['use-host-proxy'] !== undefined)
useHostProxy: !!this._environmentService.args['use-host-proxy']
};
}
private async _getTranslations(language: string): Promise<Translations> {
const config = await getNLSConfiguration(language, this.environmentService.userDataPath);
if (InternalNLSConfiguration.is(config)) {
try {
const content = await 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)
this._scanBuiltinExtensions(language),
this._scanInstalledExtensions(language),
this._scanDevelopedExtensions(language, extensionDevelopmentPath)
]);
let result = new Map<string, IExtensionDescription>();
@@ -408,101 +341,30 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel {
return r;
}
private _scanDevelopedExtensions(language: string, translations: Translations, extensionDevelopmentPaths?: string[]): Promise<IExtensionDescription[]> {
private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise<IExtensionDescription[]> {
if (extensionDevelopmentPaths) {
const extDescsP = extensionDevelopmentPaths.map(extDevPath => {
return ExtensionScanner.scanOneOrMultipleExtensions(
new ExtensionScannerInput(
this.productService.version,
this.productService.date,
this.productService.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 (await Promise.all(extensionDevelopmentPaths.map(extensionDevelopmentPath => this._extensionsScannerService.scanOneOrMultipleExtensions(URI.file(resolve(extensionDevelopmentPath)), ExtensionType.User, { language }))))
.flat()
.map(e => toExtensionDescription(e, true));
}
return Promise.resolve([]);
return [];
}
private _scanBuiltinExtensions(language: string, translations: Translations): Promise<IExtensionDescription[]> {
const version = this.productService.version;
const commit = this.productService.commit;
const date = this.productService.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(this.productService.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 async _scanBuiltinExtensions(language: string): Promise<IExtensionDescription[]> {
const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true });
return scannedExtensions.map(e => toExtensionDescription(e, false));
}
private _scanInstalledExtensions(language: string, translations: Translations): Promise<IExtensionDescription[]> {
const devMode = !!process.env['VSCODE_DEV'];
const input = new ExtensionScannerInput(
this.productService.version,
this.productService.date,
this.productService.commit,
language,
devMode,
this.environmentService.extensionsPath!,
false, // isBuiltin
false, // isUnderDevelopment
translations
);
return ExtensionScanner.scanExtensions(input, this._logger);
private async _scanInstalledExtensions(language: string): Promise<IExtensionDescription[]> {
const scannedExtensions = await this._extensionsScannerService.scanUserExtensions({ language, useCache: true });
return scannedExtensions.map(e => toExtensionDescription(e, false));
}
private _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string, translations: Translations): Promise<IExtensionDescription | null> {
const devMode = !!process.env['VSCODE_DEV'];
const input = new ExtensionScannerInput(
this.productService.version,
this.productService.date,
this.productService.commit,
language,
devMode,
extensionPath,
isBuiltin,
false, // isUnderDevelopment
translations
);
return ExtensionScanner.scanSingleExtension(input, this._logger);
private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string): Promise<IExtensionDescription | null> {
const extensionLocation = URI.file(resolve(extensionPath));
const type = isBuiltin ? ExtensionType.System : ExtensionType.User;
const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { language });
return scannedExtension ? toExtensionDescription(scannedExtension, false) : null;
}
}

View File

@@ -12,7 +12,7 @@ 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 { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } 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';
@@ -26,7 +26,7 @@ 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 { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/node/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';
@@ -36,6 +36,12 @@ 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';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
import { buildHelpMessage, buildVersionMessage, OptionDescriptions } from 'vs/platform/environment/node/argv';
import { isWindows } from 'vs/base/common/platform';
import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { ExtensionsScannerService } from 'vs/server/node/extensionsScannerService';
class CliMain extends Disposable {
@@ -73,7 +79,7 @@ class CliMain extends Disposable {
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)));
const logService: ILogService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, false, getLogLevel(environmentService)));
services.set(ILogService, logService);
logService.trace(`Remote configuration data at ${this.remoteDataFolder}`);
logService.trace('process arguments:', this.args);
@@ -89,10 +95,12 @@ class CliMain extends Disposable {
await configurationService.initialize();
services.set(IConfigurationService, configurationService);
services.set(IUriIdentityService, new UriIdentityService(fileService));
services.set(IRequestService, new SyncDescriptor(RequestService));
services.set(IDownloadService, new SyncDescriptor(DownloadService));
services.set(ITelemetryService, NullTelemetryService);
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService));
services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
services.set(IExtensionManagementCLIService, new SyncDescriptor(ExtensionManagementCLIService));
services.set(ILocalizationsService, new SyncDescriptor(LocalizationsService));
@@ -109,7 +117,8 @@ class CliMain extends Disposable {
// 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']);
const installOptions: InstallOptions = { isMachineScoped: !!this.args['do-not-sync'], installPreReleaseVersion: !!this.args['pre-release'] };
return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.args['install-extension'] || []), this.args['install-builtin-extension'] || [], installOptions, !!this.args['force']);
}
// Uninstall Extension
@@ -132,7 +141,19 @@ function eventuallyExit(code: number): void {
setTimeout(() => process.exit(code), 0);
}
export async function run(args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise<void> {
export async function run(args: ServerParsedArgs, REMOTE_DATA_FOLDER: string, optionDescriptions: OptionDescriptions<ServerParsedArgs>): Promise<void> {
if (args.help) {
const executable = product.serverApplicationName + (isWindows ? '.cmd' : '');
console.log(buildHelpMessage(product.nameLong, executable, product.version, optionDescriptions, { noInputFiles: true, noPipe: true }));
return;
}
// Version Info
if (args.version) {
console.log(buildVersionMessage(product.version, product.commit));
return;
}
const cliMain = new CliMain(args, REMOTE_DATA_FOLDER);
try {
await cliMain.run();

View File

@@ -7,189 +7,38 @@ import * as crypto from 'crypto';
import * as fs from 'fs';
import * as http from 'http';
import * as net from 'net';
import * as url from 'url';
import { release, hostname } from 'os';
import * as perf from 'vs/base/common/performance';
import { performance } from 'perf_hooks';
import { VSBuffer } from 'vs/base/common/buffer';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { generateUuid } from 'vs/base/common/uuid';
import { Promises } from 'vs/base/node/pfs';
import { findFreePort } from 'vs/base/node/ports';
import * as platform from 'vs/base/common/platform';
import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net';
import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { ConnectionType, ConnectionTypeRequest, ErrorMessage, HandshakeMessage, IRemoteExtensionHostStartParams, ITunnelConnectionStartParams, SignRequest } from 'vs/platform/remote/common/remoteAgentConnection';
import { ExtensionHostConnection } from 'vs/server/extensionHostConnection';
import { ManagementConnection } from 'vs/server/remoteExtensionManagement';
import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer';
import { ILogService, LogLevel, AbstractLogger, DEFAULT_LOG_LEVEL, MultiplexLogService, getLogLevel, LogService } from 'vs/platform/log/common/log';
import { FileAccess, Schemas } from 'vs/base/common/network';
import product from 'vs/platform/product/common/product';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IRequestService } from 'vs/platform/request/common/request';
import { RequestService } from 'vs/platform/request/node/requestService';
import { ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { IDownloadService } from 'vs/platform/download/common/download';
import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
import { ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties';
import { getMachineId } from 'vs/base/node/id';
import { FileService } from 'vs/platform/files/common/fileService';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { IFileService } from 'vs/platform/files/common/files';
import { IProductService } from 'vs/platform/product/common/productService';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IPCServer, ClientConnectionEvent, IMessagePassingProtocol, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { Emitter, Event } from 'vs/base/common/event';
import { RemoteAgentEnvironmentChannel } from 'vs/server/remoteAgentEnvironmentImpl';
import { RemoteAgentFileSystemProviderChannel } from 'vs/server/remoteFileSystemProviderIpc';
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel';
import { RequestChannel } from 'vs/platform/request/common/requestIpc';
import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import ErrorTelemetry from 'vs/platform/telemetry/node/errorTelemetry';
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
import { LogLevelChannel } from 'vs/platform/log/common/logIpc';
import { IURITransformer } from 'vs/base/common/uriIpc';
import { WebClientServer, serveError, serveFile } from 'vs/server/webClientServer';
import { URI } from 'vs/base/common/uri';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/serverEnvironmentService';
import { basename, dirname, join } from 'vs/base/common/path';
import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel';
import { RemoteTerminalChannel } from 'vs/server/remoteTerminalChannel';
import * as url from 'url';
import { LoaderStats } from 'vs/base/common/amd';
import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService';
import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService';
import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog';
import { IPtyService, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService';
import { IRemoteTelemetryService, RemoteNullTelemetryService, RemoteTelemetryService } from 'vs/server/remoteTelemetryService';
import { VSBuffer } from 'vs/base/common/buffer';
import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { connectionTokenQueryName, FileAccess, Schemas } from 'vs/base/common/network';
import { dirname, join } from 'vs/base/common/path';
import * as perf from 'vs/base/common/performance';
import * as platform from 'vs/base/common/platform';
import { createRegExp, escapeRegExpCharacters } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { findFreePort } from 'vs/base/node/ports';
import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { ConnectionType, ConnectionTypeRequest, ErrorMessage, HandshakeMessage, IRemoteExtensionHostStartParams, ITunnelConnectionStartParams, SignRequest } from 'vs/platform/remote/common/remoteAgentConnection';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionHostConnection } from 'vs/server/node/extensionHostConnection';
import { ManagementConnection } from 'vs/server/node/remoteExtensionManagement';
import { determineServerConnectionToken, requestHasValidConnectionToken as httpRequestHasValidConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken';
import { IServerEnvironmentService, ServerParsedArgs } from 'vs/server/node/serverEnvironmentService';
import { setupServerServices, SocketServer } from 'vs/server/node/serverServices';
import { CacheControl, serveError, serveFile, WebClientServer } from 'vs/server/node/webClientServer';
const SHUTDOWN_TIMEOUT = 5 * 60 * 1000;
const eventPrefix = 'monacoworkbench';
class SocketServer<TContext = string> extends IPCServer<TContext> {
private _onDidConnectEmitter: Emitter<ClientConnectionEvent>;
constructor() {
const emitter = new Emitter<ClientConnectionEvent>();
super(emitter.event);
this._onDidConnectEmitter = emitter;
}
public acceptConnection(protocol: IMessagePassingProtocol, onDidClientDisconnect: Event<void>): void {
this._onDidConnectEmitter.fire({ protocol, onDidClientDisconnect });
}
}
function twodigits(n: number): string {
if (n < 10) {
return `0${n}`;
}
return String(n);
}
function now(): string {
const date = new Date();
return `${twodigits(date.getHours())}:${twodigits(date.getMinutes())}:${twodigits(date.getSeconds())}`;
}
class ServerLogService extends AbstractLogger implements ILogService {
_serviceBrand: undefined;
private useColors: boolean;
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super();
this.setLevel(logLevel);
this.useColors = Boolean(process.stdout.isTTY);
}
trace(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Trace) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.log(`[${now()}]`, message, ...args);
}
}
}
debug(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Debug) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.log(`[${now()}]`, message, ...args);
}
}
}
info(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Info) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.log(`[${now()}]`, message, ...args);
}
}
}
warn(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Warning) {
if (this.useColors) {
console.warn(`\x1b[93m[${now()}]\x1b[0m`, message, ...args);
} else {
console.warn(`[${now()}]`, message, ...args);
}
}
}
error(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Error) {
if (this.useColors) {
console.error(`\x1b[91m[${now()}]\x1b[0m`, message, ...args);
} else {
console.error(`[${now()}]`, message, ...args);
}
}
}
critical(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Critical) {
if (this.useColors) {
console.error(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.error(`[${now()}]`, message, ...args);
}
}
}
override dispose(): void {
// noop
}
flush(): void {
// noop
}
}
export type ServerListenOptions = { host?: string; port?: number; socketPath?: string };
declare module vsda {
// the signer is a native module that for historical reasons uses a lower case class name
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -204,158 +53,40 @@ declare module vsda {
}
}
export class RemoteExtensionHostAgentServer extends Disposable {
export class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
private readonly _logService: ILogService;
private readonly _socketServer: SocketServer<RemoteAgentConnectionContext>;
private readonly _uriTransformerCache: { [remoteAuthority: string]: IURITransformer; };
private readonly _extHostConnections: { [reconnectionToken: string]: ExtensionHostConnection; };
private readonly _managementConnections: { [reconnectionToken: string]: ManagementConnection; };
private readonly _extHostConnections: { [reconnectionToken: string]: ExtensionHostConnection };
private readonly _managementConnections: { [reconnectionToken: string]: ManagementConnection };
private readonly _allReconnectionTokens: Set<string>;
private readonly _webClientServer: WebClientServer | null;
private readonly _webEndpointOriginChecker = WebEndpointOriginChecker.create(this._productService);
private shutdownTimer: NodeJS.Timer | undefined;
constructor(
private readonly _environmentService: IServerEnvironmentService,
private readonly _productService: IProductService,
private readonly _connectionToken: string,
private readonly _connectionTokenIsMandatory: boolean,
private readonly _socketServer: SocketServer<RemoteAgentConnectionContext>,
private readonly _connectionToken: ServerConnectionToken,
private readonly _vsdaMod: typeof vsda | null,
hasWebClient: boolean,
REMOTE_DATA_FOLDER: string
@IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService,
@IProductService private readonly _productService: IProductService,
@ILogService private readonly _logService: ILogService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
const logService = getOrCreateSpdLogService(this._environmentService);
logService.trace(`Remote configuration data at ${REMOTE_DATA_FOLDER}`);
logService.trace('process arguments:', this._environmentService.args);
const serverGreeting = _productService.serverGreeting.join('\n');
if (serverGreeting) {
logService.info(`\n\n${serverGreeting}\n\n`);
}
this._logService = new MultiplexLogService([new ServerLogService(getLogLevel(this._environmentService)), logService]);
this._socketServer = new SocketServer<RemoteAgentConnectionContext>();
this._uriTransformerCache = Object.create(null);
this._extHostConnections = Object.create(null);
this._managementConnections = Object.create(null);
this._allReconnectionTokens = new Set<string>();
if (hasWebClient) {
this._webClientServer = new WebClientServer(this._connectionToken, this._environmentService, this._logService, this._productService);
} else {
this._webClientServer = null;
}
this._webClientServer = (
hasWebClient
? this._instantiationService.createInstance(WebClientServer, this._connectionToken)
: null
);
this._logService.info(`Extension host agent started.`);
}
public async initialize(): Promise<{ telemetryService: ITelemetryService; }> {
const services = await this._createServices();
setTimeout(() => this._cleanupOlderLogs(this._environmentService.logsPath).then(null, err => this._logService.error(err)), 10000);
return services;
}
private async _createServices(): Promise<{ telemetryService: ITelemetryService; }> {
const services = new ServiceCollection();
// ExtensionHost Debug broadcast service
this._socketServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());
// TODO: @Sandy @Joao need dynamic context based router
const router = new StaticRouter<RemoteAgentConnectionContext>(ctx => ctx.clientId === 'renderer');
this._socketServer.registerChannel('logger', new LogLevelChannel(this._logService));
services.set(IEnvironmentService, this._environmentService);
services.set(INativeEnvironmentService, this._environmentService);
services.set(ILogService, this._logService);
services.set(IProductService, this._productService);
// Files
const fileService = this._register(new FileService(this._logService));
services.set(IFileService, fileService);
fileService.registerProvider(Schemas.file, this._register(new DiskFileSystemProvider(this._logService)));
const configurationService = new ConfigurationService(this._environmentService.machineSettingsResource, fileService);
services.set(IConfigurationService, configurationService);
services.set(IRequestService, new SyncDescriptor(RequestService));
let appInsightsAppender: ITelemetryAppender = NullAppender;
if (!this._environmentService.args['disable-telemetry'] && this._productService.enableTelemetry) {
if (this._productService.aiConfig && this._productService.aiConfig.asimovKey) {
appInsightsAppender = new AppInsightsAppender(eventPrefix, null, this._productService.aiConfig.asimovKey);
this._register(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
}
const machineId = await getMachineId();
const config: ITelemetryServiceConfig = {
appenders: [appInsightsAppender],
commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, this._productService.commit, this._productService.version + '-remote', machineId, this._productService.msftInternalDomains, this._environmentService.installSourcePath, 'remoteAgent'),
piiPaths: [this._environmentService.appRoot]
};
services.set(IRemoteTelemetryService, new SyncDescriptor(RemoteTelemetryService, [config]));
} else {
services.set(IRemoteTelemetryService, RemoteNullTelemetryService);
}
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService));
const downloadChannel = this._socketServer.getChannel('download', router);
services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => this._getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */));
services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
const instantiationService = new InstantiationService(services);
services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService));
const extensionManagementCLIService = instantiationService.createInstance(ExtensionManagementCLIService);
services.set(IExtensionManagementCLIService, extensionManagementCLIService);
const ptyService = instantiationService.createInstance(
PtyHostService,
{
GraceTime: ProtocolConstants.ReconnectionGraceTime,
ShortGraceTime: ProtocolConstants.ReconnectionShortGraceTime,
scrollback: configurationService.getValue<number>(TerminalSettingId.PersistentSessionScrollback) ?? 100
}
);
services.set(IPtyService, ptyService);
return instantiationService.invokeFunction(accessor => {
const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(this._connectionToken, this._environmentService, extensionManagementCLIService, this._logService, accessor.get(IRemoteTelemetryService), appInsightsAppender, this._productService);
this._socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel);
this._socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(this._environmentService, this._logService, ptyService, this._productService));
const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(this._logService, this._environmentService);
this._socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel);
this._socketServer.registerChannel('request', new RequestChannel(accessor.get(IRequestService)));
const extensionManagementService = accessor.get(IExtensionManagementService);
const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => this._getUriTransformer(ctx.remoteAuthority));
this._socketServer.registerChannel('extensions', channel);
// clean up deprecated extensions
(extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions();
this._register(new ErrorTelemetry(accessor.get(ITelemetryService)));
return {
telemetryService: accessor.get(ITelemetryService)
};
});
}
private _getUriTransformer(remoteAuthority: string): IURITransformer {
if (!this._uriTransformerCache[remoteAuthority]) {
this._uriTransformerCache[remoteAuthority] = createRemoteURITransformer(remoteAuthority);
}
return this._uriTransformerCache[remoteAuthority];
}
public async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
public async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<any> {
// Only serve GET requests
if (req.method !== 'GET') {
return serveError(req, res, 405, `Unsupported method ${req.method}`);
@@ -385,13 +116,14 @@ export class RemoteExtensionHostAgentServer extends Disposable {
return res.end('OK');
}
if (!httpRequestHasValidConnectionToken(this._connectionToken, req, parsedUrl)) {
// invalid connection token
return serveError(req, res, 403, `Forbidden.`);
}
if (pathname === '/vscode-remote-resource') {
// Handle HTTP requests for resources rendered in the rich client (images, fonts, etc.)
// These resources could be files shipped with extensions or even workspace files.
if (parsedUrl.query['tkn'] !== this._connectionToken) {
return serveError(req, res, 403, `Forbidden.`);
}
const desiredPath = parsedUrl.query['path'];
if (typeof desiredPath !== 'string') {
return serveError(req, res, 400, `Bad request.`);
@@ -412,7 +144,14 @@ export class RemoteExtensionHostAgentServer extends Disposable {
responseHeaders['Cache-Control'] = 'public, max-age=31536000';
}
}
return serveFile(this._logService, req, res, filePath, responseHeaders);
// Allow cross origin requests from the web worker extension host
responseHeaders['Vary'] = 'Origin';
const requestOrigin = req.headers['origin'];
if (requestOrigin && this._webEndpointOriginChecker.matches(requestOrigin)) {
responseHeaders['Access-Control-Allow-Origin'] = requestOrigin;
}
return serveFile(filePath, CacheControl.ETAG, this._logService, req, res, responseHeaders);
}
// workbench web UI
@@ -487,12 +226,14 @@ export class RemoteExtensionHostAgentServer extends Disposable {
// Never timeout this socket due to inactivity!
socket.setTimeout(0);
// Disable Nagle's algorithm
socket.setNoDelay(true);
// Finally!
if (skipWebSocketFrames) {
this._handleWebSocketConnection(new NodeSocket(socket), isReconnection, reconnectionToken);
this._handleWebSocketConnection(new NodeSocket(socket, `server-connection-${reconnectionToken}`), isReconnection, reconnectionToken);
} else {
this._handleWebSocketConnection(new WebSocketNodeSocket(new NodeSocket(socket), permessageDeflate, null, true), isReconnection, reconnectionToken);
this._handleWebSocketConnection(new WebSocketNodeSocket(new NodeSocket(socket, `server-connection-${reconnectionToken}`), permessageDeflate, null, true), isReconnection, reconnectionToken);
}
}
@@ -502,19 +243,6 @@ export class RemoteExtensionHostAgentServer extends Disposable {
}
// Eventually cleanup
/**
* Cleans up older logs, while keeping the 10 most recent ones.
*/
private async _cleanupOlderLogs(logsPath: string): Promise<void> {
const currentLog = basename(logsPath);
const logsRoot = dirname(logsPath);
const children = await Promises.readdir(logsRoot);
const allSessions = children.filter(name => /^\d{8}T\d{6}$/.test(name));
const oldSessions = allSessions.sort().filter((d) => d !== currentLog);
const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9));
await Promise.all(toDelete.map(name => Promises.rm(join(logsRoot, name))));
}
private _getRemoteAddress(socket: NodeSocket | WebSocketNodeSocket): string {
let _socket: net.Socket;
@@ -549,14 +277,8 @@ export class RemoteExtensionHostAgentServer extends Disposable {
const logPrefix = `[${remoteAddress}][${reconnectionToken.substr(0, 8)}]`;
const protocol = new PersistentProtocol(socket);
let validator: vsda.validator;
let signer: vsda.signer;
try {
const vsdaMod = <typeof vsda>require.__$__nodeRequire('vsda');
validator = new vsdaMod.validator();
signer = new vsdaMod.signer();
} catch (e) {
}
const validator = this._vsdaMod ? new this._vsdaMod.validator() : null;
const signer = this._vsdaMod ? new this._vsdaMod.signer() : null;
const enum State {
WaitingForAuth,
@@ -584,7 +306,7 @@ export class RemoteExtensionHostAgentServer extends Disposable {
return rejectWebSocketConnection(`Invalid first message`);
}
if (this._connectionTokenIsMandatory && msg1.auth !== this._connectionToken) {
if (this._connectionToken.type === ServerConnectionTokenType.Mandatory && !this._connectionToken.validate(msg1.auth)) {
return rejectWebSocketConnection(`Unauthorized client refused: auth mismatch`);
}
@@ -639,7 +361,7 @@ export class RemoteExtensionHostAgentServer extends Disposable {
let valid = false;
if (!validator) {
valid = true;
} else if (msg2.signedData === this._connectionToken) {
} else if (this._connectionToken.validate(msg2.signedData)) {
// web client
valid = true;
} else {
@@ -747,6 +469,7 @@ export class RemoteExtensionHostAgentServer extends Disposable {
}
}
protocol.sendPause();
protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {})));
const dataChunk = protocol.readEntireBuffer();
protocol.dispose();
@@ -759,10 +482,11 @@ export class RemoteExtensionHostAgentServer extends Disposable {
return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`);
}
protocol.sendPause();
protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {})));
const dataChunk = protocol.readEntireBuffer();
protocol.dispose();
const con = new ExtensionHostConnection(this._environmentService, this._logService, reconnectionToken, remoteAddress, socket, dataChunk);
const con = this._instantiationService.createInstance(ExtensionHostConnection, reconnectionToken, remoteAddress, socket, dataChunk);
this._extHostConnections[reconnectionToken] = con;
this._allReconnectionTokens.add(reconnectionToken);
con.onClose(() => {
@@ -905,57 +629,70 @@ export class RemoteExtensionHostAgentServer extends Disposable {
}
}
function parseConnectionToken(args: ServerParsedArgs): { connectionToken: string; connectionTokenIsMandatory: boolean; } {
if (args['connection-secret']) {
if (args['connectionToken']) {
console.warn(`Please do not use the argument connectionToken at the same time as connection-secret.`);
process.exit(1);
}
let rawConnectionToken = fs.readFileSync(args['connection-secret']).toString();
rawConnectionToken = rawConnectionToken.replace(/\r?\n$/, '');
if (!/^[0-9A-Za-z\-]+$/.test(rawConnectionToken)) {
console.warn(`The secret defined in ${args['connection-secret']} does not adhere to the characters 0-9, a-z, A-Z or -.`);
process.exit(1);
}
return { connectionToken: rawConnectionToken, connectionTokenIsMandatory: true };
} else {
return { connectionToken: args['connectionToken'] || generateUuid(), connectionTokenIsMandatory: false };
}
}
export interface IServerAPI {
/**
* Do not remove!!. Called from vs/server/main.js
* Do not remove!!. Called from server-main.js
*/
handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
/**
* Do not remove!!. Called from vs/server/main.js
* Do not remove!!. Called from server-main.js
*/
handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void;
/**
* Do not remove!!. Called from vs/server/main.js
* Do not remove!!. Called from server-main.js
*/
handleServerError(err: Error): void;
/**
* Do not remove!!. Called from vs/server/main.js
* Do not remove!!. Called from server-main.js
*/
dispose(): void;
}
export async function createServer(address: string | net.AddressInfo | null, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise<IServerAPI> {
const productService = { _serviceBrand: undefined, ...product };
const environmentService = new ServerEnvironmentService(args, productService);
const connectionToken = await determineServerConnectionToken(args);
if (connectionToken instanceof ServerConnectionTokenParseError) {
console.warn(connectionToken.message);
process.exit(1);
}
const disposables = new DisposableStore();
const { socketServer, instantiationService } = await setupServerServices(connectionToken, args, REMOTE_DATA_FOLDER, disposables);
// Set the unexpected error handler after the services have been initialized, to avoid having
// the telemetry service overwrite our handler
instantiationService.invokeFunction((accessor) => {
const logService = accessor.get(ILogService);
setUnexpectedErrorHandler(err => {
// See https://github.com/microsoft/vscode-remote-release/issues/6481
// In some circumstances, console.error will throw an asynchronous error. This asynchronous error
// will end up here, and then it will be logged again, thus creating an endless asynchronous loop.
// Here we try to break the loop by ignoring EPIPE errors that include our own unexpected error handler in the stack.
if (err && err.code === 'EPIPE' && err.syscall === 'write' && err.stack && /unexpectedErrorHandler/.test(err.stack)) {
return;
}
logService.error(err);
});
process.on('SIGPIPE', () => {
// See https://github.com/microsoft/vscode-remote-release/issues/6543
// We would normally install a SIGPIPE listener in bootstrap.js
// But in certain situations, the console itself can be in a broken pipe state
// so logging SIGPIPE to the console will cause an infinite async loop
onUnexpectedError(new Error(`Unexpected SIGPIPE`));
});
});
//
// On Windows, exit early with warning message to users about potential security issue
// if there is node_modules folder under home drive or Users folder.
//
if (process.platform === 'win32' && process.env.HOMEDRIVE && process.env.HOMEPATH) {
const homeDirModulesPath = join(process.env.HOMEDRIVE, 'node_modules');
const userDir = dirname(join(process.env.HOMEDRIVE, process.env.HOMEPATH));
const userDirModulesPath = join(userDir, 'node_modules');
if (fs.existsSync(homeDirModulesPath) || fs.existsSync(userDirModulesPath)) {
const message = `
instantiationService.invokeFunction((accessor) => {
const logService = accessor.get(ILogService);
if (process.platform === 'win32' && process.env.HOMEDRIVE && process.env.HOMEPATH) {
const homeDirModulesPath = join(process.env.HOMEDRIVE, 'node_modules');
const userDir = dirname(join(process.env.HOMEDRIVE, process.env.HOMEPATH));
const userDirModulesPath = join(userDir, 'node_modules');
if (fs.existsSync(homeDirModulesPath) || fs.existsSync(userDirModulesPath)) {
const message = `
*
* !!!! Server terminated due to presence of CVE-2020-1416 !!!!
@@ -968,24 +705,35 @@ export async function createServer(address: string | net.AddressInfo | null, arg
*
`;
const logService = getOrCreateSpdLogService(environmentService);
logService.warn(message);
console.warn(message);
process.exit(0);
logService.warn(message);
console.warn(message);
process.exit(0);
}
}
}
});
const vsdaMod = instantiationService.invokeFunction((accessor) => {
const logService = accessor.get(ILogService);
const hasVSDA = fs.existsSync(join(FileAccess.asFileUri('', require).fsPath, '../node_modules/vsda'));
if (hasVSDA) {
try {
return <typeof vsda>require.__$__nodeRequire('vsda');
} catch (err) {
logService.error(err);
}
}
return null;
});
const { connectionToken, connectionTokenIsMandatory } = parseConnectionToken(args);
const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/code/browser/workbench/workbench.html', require).fsPath);
if (hasWebClient && address && typeof address !== 'string') {
// ships the web ui!
console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}/?tkn=${connectionToken}`);
const queryPart = (connectionToken.type !== ServerConnectionTokenType.None ? `?${connectionTokenQueryName}=${connectionToken.value}` : '');
console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}/${queryPart}`);
}
const remoteExtensionHostAgentServer = new RemoteExtensionHostAgentServer(environmentService, productService, connectionToken, connectionTokenIsMandatory, hasWebClient, REMOTE_DATA_FOLDER);
const services = await remoteExtensionHostAgentServer.initialize();
const { telemetryService } = services;
const remoteExtensionHostAgentServer = instantiationService.createInstance(RemoteExtensionHostAgentServer, socketServer, connectionToken, vsdaMod, hasWebClient);
perf.mark('code/server/ready');
const currentTime = performance.now();
@@ -993,23 +741,27 @@ export async function createServer(address: string | net.AddressInfo | null, arg
const vscodeServerListenTime: number = (<any>global).vscodeServerListenTime;
const vscodeServerCodeLoadedTime: number = (<any>global).vscodeServerCodeLoadedTime;
type ServerStartClassification = {
startTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
startedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
codeLoadedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
readyTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' };
};
type ServerStartEvent = {
startTime: number;
startedTime: number;
codeLoadedTime: number;
readyTime: number;
};
telemetryService.publicLog2<ServerStartEvent, ServerStartClassification>('serverStart', {
startTime: vscodeServerStartTime,
startedTime: vscodeServerListenTime,
codeLoadedTime: vscodeServerCodeLoadedTime,
readyTime: currentTime
instantiationService.invokeFunction((accessor) => {
const telemetryService = accessor.get(ITelemetryService);
type ServerStartClassification = {
startTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' };
startedTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' };
codeLoadedTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' };
readyTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' };
};
type ServerStartEvent = {
startTime: number;
startedTime: number;
codeLoadedTime: number;
readyTime: number;
};
telemetryService.publicLog2<ServerStartEvent, ServerStartClassification>('serverStart', {
startTime: vscodeServerStartTime,
startedTime: vscodeServerListenTime,
codeLoadedTime: vscodeServerCodeLoadedTime,
readyTime: currentTime
});
});
if (args['print-startup-performance']) {
@@ -1032,12 +784,44 @@ export async function createServer(address: string | net.AddressInfo | null, arg
return remoteExtensionHostAgentServer;
}
const getOrCreateSpdLogService: (environmentService: IServerEnvironmentService) => ILogService = (function () {
let _logService: ILogService | null;
return function getLogService(environmentService: IServerEnvironmentService): ILogService {
if (!_logService) {
_logService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, getLogLevel(environmentService)));
class WebEndpointOriginChecker {
public static create(productService: IProductService): WebEndpointOriginChecker {
const webEndpointUrlTemplate = productService.webEndpointUrlTemplate;
const commit = productService.commit;
const quality = productService.quality;
if (!webEndpointUrlTemplate || !commit || !quality) {
return new WebEndpointOriginChecker(null);
}
return _logService;
};
})();
const uuid = generateUuid();
const exampleUrl = new URL(
webEndpointUrlTemplate
.replace('{{uuid}}', uuid)
.replace('{{commit}}', commit)
.replace('{{quality}}', quality)
);
const exampleOrigin = exampleUrl.origin;
const originRegExpSource = (
escapeRegExpCharacters(exampleOrigin)
.replace(uuid, '[a-zA-Z0-9\\-]+')
);
try {
const originRegExp = createRegExp(`^${originRegExpSource}$`, true, { matchCase: false });
return new WebEndpointOriginChecker(originRegExp);
} catch (err) {
return new WebEndpointOriginChecker(null);
}
}
constructor(
private readonly _originRegExp: RegExp | null
) { }
public matches(origin: string): boolean {
if (!this._originRegExp) {
return false;
}
return this._originRegExp.test(origin);
}
}

View File

@@ -4,17 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter } 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 { IFileChange, IWatchOptions } from 'vs/platform/files/common/files';
import { IFileChange } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer';
import { createURITransformer } from 'vs/workbench/api/node/uriTransformer';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { DiskFileSystemProvider, IWatcherOptions } from 'vs/platform/files/node/diskFileSystemProvider';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { posix, delimiter } from 'vs/base/common/path';
import { IServerEnvironmentService } from 'vs/server/serverEnvironmentService';
import { AbstractDiskFileSystemProviderChannel, ISessionFileWatcher } from 'vs/platform/files/node/diskFileSystemProviderIpc';
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
import { AbstractDiskFileSystemProviderChannel, AbstractSessionFileWatcher, ISessionFileWatcher } from 'vs/platform/files/node/diskFileSystemProviderServer';
import { IRecursiveWatcherOptions } from 'vs/platform/files/common/watcher';
export class RemoteAgentFileSystemProviderChannel extends AbstractDiskFileSystemProviderChannel<RemoteAgentConnectionContext> {
@@ -32,7 +32,7 @@ export class RemoteAgentFileSystemProviderChannel extends AbstractDiskFileSystem
protected override getUriTransformer(ctx: RemoteAgentConnectionContext): IURITransformer {
let transformer = this.uriTransformerCache.get(ctx.remoteAuthority);
if (!transformer) {
transformer = createRemoteURITransformer(ctx.remoteAuthority);
transformer = createURITransformer(ctx.remoteAuthority);
this.uriTransformerCache.set(ctx.remoteAuthority, transformer);
}
@@ -58,40 +58,19 @@ export class RemoteAgentFileSystemProviderChannel extends AbstractDiskFileSystem
//#endregion
}
class SessionFileWatcher extends Disposable implements ISessionFileWatcher {
private readonly watcherRequests = new Map<number, IDisposable>();
private readonly fileWatcher = this._register(new DiskFileSystemProvider(this.logService, { watcher: this.getWatcherOptions() }));
class SessionFileWatcher extends AbstractSessionFileWatcher {
constructor(
private readonly uriTransformer: IURITransformer,
uriTransformer: IURITransformer,
sessionEmitter: Emitter<IFileChange[] | string>,
private readonly logService: ILogService,
private readonly environmentService: IServerEnvironmentService
logService: ILogService,
environmentService: IServerEnvironmentService
) {
super();
this.registerListeners(sessionEmitter);
super(uriTransformer, sessionEmitter, logService, environmentService);
}
private registerListeners(sessionEmitter: Emitter<IFileChange[] | string>): void {
const localChangeEmitter = this._register(new Emitter<readonly IFileChange[]>());
this._register(localChangeEmitter.event((events) => {
sessionEmitter.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 => sessionEmitter.fire(error)));
}
private getWatcherOptions(): IWatcherOptions | undefined {
const fileWatcherPolling = this.environmentService.args['fileWatcherPolling'];
protected override getRecursiveWatcherOptions(environmentService: IServerEnvironmentService): IRecursiveWatcherOptions | undefined {
const fileWatcherPolling = environmentService.args['file-watcher-polling'];
if (fileWatcherPolling) {
const segments = fileWatcherPolling.split(delimiter);
const pollingInterval = Number(segments[0]);
@@ -104,25 +83,13 @@ class SessionFileWatcher extends Disposable implements ISessionFileWatcher {
return undefined;
}
watch(req: number, resource: URI, opts: IWatchOptions): IDisposable {
if (this.environmentService.extensionsPath) {
protected override getExtraExcludes(environmentService: IServerEnvironmentService): string[] | undefined {
if (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, '**')];
return [posix.join(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();
return undefined;
}
}

View File

@@ -15,10 +15,10 @@ 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 { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IPtyService, IShellLaunchConfig, ITerminalProfile, ITerminalsLayoutInfo } from 'vs/platform/terminal/common/terminal';
import { IPtyService, IShellLaunchConfig, ITerminalProfile } 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 { createURITransformer } from 'vs/workbench/api/node/uriTransformer';
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';
@@ -26,16 +26,18 @@ import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/t
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';
import { buildUserEnvironment } from 'vs/server/node/extensionHostConnection';
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
import { IProductService } from 'vs/platform/product/common/productService';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
class CustomVariableResolver extends AbstractVariableResolverService {
constructor(
env: platform.IProcessEnvironment,
workspaceFolders: IWorkspaceFolder[],
activeFileResource: URI | undefined,
resolvedVariables: { [name: string]: string; }
resolvedVariables: { [name: string]: string },
extensionService: IExtensionManagementService,
) {
super({
getFolderUri: (folderName: string): URI | undefined => {
@@ -68,8 +70,13 @@ class CustomVariableResolver extends AbstractVariableResolverService {
},
getLineNumber: (): string | undefined => {
return resolvedVariables['lineNumber'];
}
}, undefined, Promise.resolve(env));
},
getExtension: async id => {
const installed = await extensionService.getInstalled();
const found = installed.find(e => e.identifier.id === id);
return found && { extensionLocation: found.location };
},
}, undefined, Promise.resolve(os.homedir()), Promise.resolve(env));
}
}
@@ -82,14 +89,15 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
uriTransformer: IURITransformer;
}>();
private readonly _onExecuteCommand = this._register(new Emitter<{ reqId: number, commandId: string, commandArgs: any[] }>());
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,
private readonly _productService: IProductService
private readonly _productService: IProductService,
private readonly _extensionManagementService: IExtensionManagementService,
) {
super();
}
@@ -99,7 +107,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
case '$restartPtyHost': return this._ptyService.restartPtyHost?.apply(this._ptyService, args);
case '$createProcess': {
const uriTransformer = createRemoteURITransformer(ctx.remoteAuthority);
const uriTransformer = createURITransformer(ctx.remoteAuthority);
return this._createProcess(uriTransformer, <ICreateTerminalProcessArguments>args);
}
case '$attachToProcess': return this._ptyService.attachToProcess.apply(this._ptyService, args);
@@ -120,14 +128,17 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
case '$processBinary': return this._ptyService.processBinary.apply(this._ptyService, args);
case '$sendCommandResult': return this._sendCommandResult(args[0], args[1], args[2]);
case '$installAutoReply': return this._ptyService.installAutoReply.apply(this._ptyService, args);
case '$uninstallAllAutoReplies': return this._ptyService.uninstallAllAutoReplies.apply(this._ptyService, args);
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 '$getTerminalLayoutInfo': return this._ptyService.getTerminalLayoutInfo(<IGetTerminalLayoutInfoArgs>args);
case '$setTerminalLayoutInfo': return this._ptyService.setTerminalLayoutInfo(<ISetTerminalLayoutInfoArgs>args);
case '$serializeTerminalState': return this._ptyService.serializeTerminalState.apply(this._ptyService, args);
case '$reviveTerminalProcesses': return this._ptyService.reviveTerminalProcesses.apply(this._ptyService, args);
case '$setUnicodeVersion': return this._ptyService.setUnicodeVersion.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);
@@ -177,13 +188,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
};
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();
}
const baseEnv = await buildUserEnvironment(args.resolverEnv, !!args.shellLaunchConfig.useShellEnvironment, platform.language, false, this._environmentService, this._logService);
this._logService.trace('baseEnv', baseEnv);
const reviveWorkspaceFolder = (workspaceData: IWorkspaceFolderData): IWorkspaceFolder => {
@@ -199,16 +204,16 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
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 customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables, this._extensionManagementService);
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);
const initialCwd = await 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(
const env = await terminalEnvironment.createTerminalEnvironment(
shellLaunchConfig,
envFromConfig,
variableResolver,
@@ -225,7 +230,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
}
const envVariableCollections = new Map<string, IEnvironmentVariableCollection>(entries);
const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections);
mergedCollection.applyToProcessEnvironment(env);
await mergedCollection.applyToProcessEnvironment(env, variableResolver);
}
// Fork the process and listen for messages
@@ -239,7 +244,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
};
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);
const id = await this._ptyService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows, args.unicodeVersion, env, baseEnv, args.options, args.shouldPersistTerminal, args.workspaceId, args.workspaceName);
this._ptyService.onProcessExit(e => e.id === id && cliServer.dispose());
return {
@@ -314,13 +319,6 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
return this._ptyService.getWslPath(original);
}
private _setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void {
this._ptyService.setTerminalLayoutInfo(args);
}
private async _getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined> {
return this._ptyService.getTerminalLayoutInfo(args);
}
private _reduceConnectionGraceTime(): Promise<void> {
return this._ptyService.reduceConnectionGraceTime();

View File

@@ -10,7 +10,7 @@ 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 { parseArgs, buildHelpMessage, buildVersionMessage, OPTIONS, OptionDescriptions, ErrorReporter } 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';
@@ -32,7 +32,7 @@ interface ProductDescription {
executableName: string;
}
interface RemoteParsedArgs extends NativeParsedArgs { 'gitCredential'?: string; 'openExternal'?: boolean; }
interface RemoteParsedArgs extends NativeParsedArgs { 'gitCredential'?: string; 'openExternal'?: boolean }
const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => {
@@ -41,7 +41,7 @@ const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => {
case 'extensions-dir':
case 'export-default-configuration':
case 'install-source':
case 'driver':
case 'enable-smoke-test-driver':
case 'extensions-download-dir':
case 'builtin-extensions-dir':
case 'telemetry':
@@ -70,6 +70,8 @@ const isSupportedForPipe = (optionId: keyof RemoteParsedArgs) => {
case 'force':
case 'show-versions':
case 'category':
case 'verbose':
case 'remote':
return true;
default:
return false;
@@ -79,7 +81,7 @@ const isSupportedForPipe = (optionId: keyof RemoteParsedArgs) => {
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 cliRemoteAuthority = process.env['VSCODE_CLI_AUTHORITY'] as string;
const cliStdInFilePath = process.env['VSCODE_STDIN_FILE_PATH'] as string;
@@ -103,21 +105,27 @@ export function main(desc: ProductDescription, args: string[]): void {
options['openExternal'] = { type: 'boolean' };
}
const errorReporter = {
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 ${desc.executableName}.`);
},
onDeprecatedOption: (deprecatedOption: string, message: string) => {
console.warn(`Option '${deprecatedOption}' is deprecated: ${message}`);
}
};
const parsedArgs = parseArgs(args, options, errorReporter);
const mapFileUri = remoteAuthority ? mapFileToRemoteUri : (uri: string) => uri;
const mapFileUri = cliRemoteAuthority ? mapFileToRemoteUri : (uri: string) => uri;
const verbose = !!parsedArgs['verbose'];
if (parsedArgs.help) {
console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options, true));
console.log(buildHelpMessage(desc.productName, desc.executableName, desc.version, options));
return;
}
if (parsedArgs.version) {
@@ -126,19 +134,23 @@ export function main(desc: ProductDescription, args: string[]): void {
}
if (cliPipe) {
if (parsedArgs['openExternal']) {
openInBrowser(parsedArgs['_']);
openInBrowser(parsedArgs['_'], verbose);
return;
}
}
let remote: string | null | undefined = parsedArgs.remote;
if (remote === 'local' || remote === 'false' || remote === '') {
remote = null; // null represent a local window
}
let folderURIs = (parsedArgs['folder-uri'] || []).map(mapFileUri);
const folderURIs = (parsedArgs['folder-uri'] || []).map(mapFileUri);
parsedArgs['folder-uri'] = folderURIs;
let fileURIs = (parsedArgs['file-uri'] || []).map(mapFileUri);
const fileURIs = (parsedArgs['file-uri'] || []).map(mapFileUri);
parsedArgs['file-uri'] = fileURIs;
let inputPaths = parsedArgs['_'];
const inputPaths = parsedArgs['_'];
let hasReadStdinArg = false;
for (let input of inputPaths) {
if (input === '-') {
@@ -155,7 +167,7 @@ export function main(desc: ProductDescription, args: string[]): void {
let stdinFilePath = cliStdInFilePath;
if (!stdinFilePath) {
stdinFilePath = getStdinFilePath();
readFromStdin(stdinFilePath, !!parsedArgs.verbose); // throws error if file can not be written
readFromStdin(stdinFilePath, verbose); // throws error if file can not be written
}
// Make sure to open tmp file
@@ -186,10 +198,6 @@ export function main(desc: ProductDescription, args: string[]): void {
return;
}
if (remoteAuthority) {
parsedArgs['remote'] = remoteAuthority;
}
if (cliCommand) {
if (parsedArgs['install-extension'] !== undefined || parsedArgs['uninstall-extension'] !== undefined || parsedArgs['list-extensions']) {
const cmdLine: string[] = [];
@@ -201,7 +209,8 @@ export function main(desc: ProductDescription, args: string[]): void {
cmdLine.push(`--${opt}=${value}`);
}
});
const cp = _cp.fork(join(__dirname, 'main.js'), cmdLine, { stdio: 'inherit' });
const cp = _cp.fork(join(__dirname, '../../../server-main.js'), cmdLine, { stdio: 'inherit' });
cp.on('error', err => console.log(err));
return;
}
@@ -222,11 +231,14 @@ export function main(desc: ProductDescription, args: string[]): void {
newCommandline.push(`--${key}=${val.toString()}`);
}
}
if (remote !== null) {
newCommandline.push(`--remote=${remote || cliRemoteAuthority}`);
}
const ext = extname(cliCommand);
if (ext === '.bat' || ext === '.cmd') {
const processCwd = cliCommandCwd || cwd();
if (parsedArgs['verbose']) {
if (verbose) {
console.log(`Invoking: cmd.exe /C ${cliCommand} ${newCommandline.join(' ')} in ${processCwd}`);
}
_cp.spawn('cmd.exe', ['/C', cliCommand, ...newCommandline], {
@@ -236,22 +248,21 @@ export function main(desc: ProductDescription, args: string[]): void {
} else {
const cliCwd = dirname(cliCommand);
const env = { ...process.env, ELECTRON_RUN_AS_NODE: '1' };
newCommandline.unshift('--ms-enable-electron-run-as-node');
newCommandline.unshift('resources/app/out/cli.js');
if (parsedArgs['verbose']) {
console.log(`Invoking: ${cliCommand} ${newCommandline.join(' ')} in ${cliCwd}`);
if (verbose) {
console.log(`Invoking: cd "${cliCwd}" && ELECTRON_RUN_AS_NODE=1 "${cliCommand}" "${newCommandline.join('" "')}"`);
}
_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) => {
}, verbose).then((res: string) => {
console.log(res);
}).catch(e => {
console.error('Error when requesting status:', e);
});
return;
}
@@ -263,24 +274,21 @@ export function main(desc: ProductDescription, args: string[]): void {
install: asExtensionIdOrVSIX(parsedArgs['install-extension']),
uninstall: asExtensionIdOrVSIX(parsedArgs['uninstall-extension']),
force: parsedArgs['force']
}).then((res: string) => {
}, verbose).then((res: string) => {
console.log(res);
}).catch(e => {
console.error('Error when invoking the extension management command:', e);
});
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);
waitMarkerFilePath = createWaitMarkerFile(verbose);
}
sendToPipe({
@@ -292,7 +300,10 @@ export function main(desc: ProductDescription, args: string[]): void {
gotoLineMode: parsedArgs.goto,
forceReuseWindow: parsedArgs['reuse-window'],
forceNewWindow: parsedArgs['new-window'],
waitMarkerFilePath
waitMarkerFilePath,
remoteAuthority: remote
}, verbose).catch(e => {
console.error('Error when invoking the open command:', e);
});
if (waitMarkerFilePath) {
@@ -307,7 +318,7 @@ async function waitForFileDeleted(path: string) {
}
}
function openInBrowser(args: string[]) {
function openInBrowser(args: string[], verbose: boolean) {
let uris: string[] = [];
for (let location of args) {
try {
@@ -324,12 +335,17 @@ function openInBrowser(args: string[]) {
sendToPipe({
type: 'openExternal',
uris
}, verbose).catch(e => {
console.error('Error when invoking the open external command:', e);
});
}
}
function sendToPipe(args: PipeCommand): Promise<any> {
return new Promise<string>(resolve => {
function sendToPipe(args: PipeCommand, verbose: boolean): Promise<any> {
if (verbose) {
console.log(JSON.stringify(args, null, ' '));
}
return new Promise<string>((resolve, reject) => {
const message = JSON.stringify(args);
if (!cliPipe) {
console.log('Message ' + message);
@@ -340,22 +356,41 @@ function sendToPipe(args: PipeCommand): Promise<any> {
const opts: _http.RequestOptions = {
socketPath: cliPipe,
path: '/',
method: 'POST'
method: 'POST',
headers: {
'content-type': 'application/json',
'accept': 'application/json'
}
};
const req = _http.request(opts, res => {
if (res.headers['content-type'] !== 'application/json') {
reject('Error in response: Invalid content type: Expected \'application/json\', is: ' + res.headers['content-type']);
return;
}
const chunks: string[] = [];
res.setEncoding('utf8');
res.on('data', chunk => {
chunks.push(chunk);
});
res.on('error', () => fatal('Error in response'));
res.on('error', (err) => fatal('Error in response.', err));
res.on('end', () => {
resolve(chunks.join(''));
const content = chunks.join('');
try {
const obj = JSON.parse(content);
if (res.statusCode === 200) {
resolve(obj);
} else {
reject(obj);
}
} catch (e) {
reject('Error in response: Unable to parse response as JSON: ' + content);
}
});
});
req.on('error', () => fatal('Error in request'));
req.on('error', (err) => fatal('Error in request.', err));
req.write(message);
req.end();
});
@@ -365,8 +400,8 @@ 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.');
function fatal(message: string, err: any): void {
console.error('Unable to connect to VS Code server: ' + message);
console.error(err);
process.exit(1);
}
@@ -404,7 +439,7 @@ function translatePath(input: string, mapFileUri: (input: string) => string, fol
}
function mapFileToRemoteUri(uri: string): string {
return uri.replace(/^file:\/\//, 'vscode-remote://' + remoteAuthority);
return uri.replace(/^file:\/\//, 'vscode-remote://' + cliRemoteAuthority);
}
let [, , productName, version, commit, executableName, ...remainingArgs] = process.argv;

View File

@@ -7,12 +7,13 @@ 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 { run as runCli } from 'vs/server/node/remoteExtensionHostAgentCli';
import { createServer as doCreateServer, IServerAPI } from 'vs/server/node/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 { serverOptions } from 'vs/server/node/serverEnvironmentService';
import product from 'vs/platform/product/common/product';
import * as perf from 'vs/base/common/performance';
perf.mark('code/server/codeLoaded');
@@ -25,15 +26,20 @@ const errorReporter: ErrorReporter = {
onUnknownOption: (id: string) => {
console.error(`Ignoring option ${id}: not supported for server.`);
},
onDeprecatedOption: (deprecatedOption: string, message) => {
console.warn(`Option '${deprecatedOption}' is deprecated: ${message}`);
}
};
const args = parseArgs(process.argv.slice(2), serverOptions, errorReporter);
const REMOTE_DATA_FOLDER = process.env['VSCODE_AGENT_FOLDER'] || join(os.homedir(), '.vscode-remote');
const REMOTE_DATA_FOLDER = args['server-data-dir'] || process.env['VSCODE_AGENT_FOLDER'] || join(os.homedir(), product.serverDataFolderName || '.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 LOCAL_HISTORY_HOME = join(APP_SETTINGS_HOME, 'History');
const MACHINE_SETTINGS_HOME = join(USER_DATA_PATH, 'Machine');
args['user-data-dir'] = USER_DATA_PATH;
const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath);
@@ -41,23 +47,23 @@ 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 => {
[REMOTE_DATA_FOLDER, args['extensions-dir'], USER_DATA_PATH, APP_SETTINGS_HOME, MACHINE_SETTINGS_HOME, GLOBAL_STORAGE_HOME, LOCAL_HISTORY_HOME].forEach(f => {
try {
if (!fs.existsSync(f)) {
fs.mkdirSync(f);
fs.mkdirSync(f, { mode: 0o700 });
}
} catch (err) { console.error(err); }
});
/**
* invoked by vs/server/main.js
* invoked by server-main.js
*/
export function spawnCli() {
runCli(args, REMOTE_DATA_FOLDER);
runCli(args, REMOTE_DATA_FOLDER, serverOptions);
}
/**
* invoked by vs/server/main.js
* invoked by server-main.js
*/
export function createServer(address: string | net.AddressInfo | null): Promise<IServerAPI> {
return doCreateServer(address, args, REMOTE_DATA_FOLDER);

View File

@@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cookie from 'cookie';
import * as fs from 'fs';
import * as http from 'http';
import * as url from 'url';
import * as path from 'vs/base/common/path';
import { generateUuid } from 'vs/base/common/uuid';
import { connectionTokenCookieName, connectionTokenQueryName } from 'vs/base/common/network';
import { ServerParsedArgs } from 'vs/server/node/serverEnvironmentService';
import { Promises } from 'vs/base/node/pfs';
const connectionTokenRegex = /^[0-9A-Za-z-]+$/;
export const enum ServerConnectionTokenType {
None,
Optional,// TODO: Remove this soon
Mandatory
}
export class NoneServerConnectionToken {
public readonly type = ServerConnectionTokenType.None;
public validate(connectionToken: any): boolean {
return true;
}
}
export class OptionalServerConnectionToken {
public readonly type = ServerConnectionTokenType.Optional;
constructor(public readonly value: string) {
}
public validate(connectionToken: any): boolean {
return (connectionToken === this.value);
}
}
export class MandatoryServerConnectionToken {
public readonly type = ServerConnectionTokenType.Mandatory;
constructor(public readonly value: string) {
}
public validate(connectionToken: any): boolean {
return (connectionToken === this.value);
}
}
export type ServerConnectionToken = NoneServerConnectionToken | OptionalServerConnectionToken | MandatoryServerConnectionToken;
export class ServerConnectionTokenParseError {
constructor(
public readonly message: string
) { }
}
export async function parseServerConnectionToken(args: ServerParsedArgs, defaultValue: () => Promise<string>): Promise<ServerConnectionToken | ServerConnectionTokenParseError> {
const withoutConnectionToken = args['without-connection-token'];
const connectionToken = args['connection-token'];
const connectionTokenFile = args['connection-token-file'];
const compatibility = (args['compatibility'] === '1.63');
if (withoutConnectionToken) {
if (typeof connectionToken !== 'undefined' || typeof connectionTokenFile !== 'undefined') {
return new ServerConnectionTokenParseError(`Please do not use the argument '--connection-token' or '--connection-token-file' at the same time as '--without-connection-token'.`);
}
return new NoneServerConnectionToken();
}
if (typeof connectionTokenFile !== 'undefined') {
if (typeof connectionToken !== 'undefined') {
return new ServerConnectionTokenParseError(`Please do not use the argument '--connection-token' at the same time as '--connection-token-file'.`);
}
let rawConnectionToken: string;
try {
rawConnectionToken = fs.readFileSync(connectionTokenFile).toString().replace(/\r?\n$/, '');
} catch (e) {
return new ServerConnectionTokenParseError(`Unable to read the connection token file at '${connectionTokenFile}'.`);
}
if (!connectionTokenRegex.test(rawConnectionToken)) {
return new ServerConnectionTokenParseError(`The connection token defined in '${connectionTokenFile} does not adhere to the characters 0-9, a-z, A-Z or -.`);
}
return new MandatoryServerConnectionToken(rawConnectionToken);
}
if (typeof connectionToken !== 'undefined') {
if (!connectionTokenRegex.test(connectionToken)) {
return new ServerConnectionTokenParseError(`The connection token '${connectionToken} does not adhere to the characters 0-9, a-z, A-Z or -.`);
}
if (compatibility) {
// TODO: Remove this case soon
return new OptionalServerConnectionToken(connectionToken);
}
return new MandatoryServerConnectionToken(connectionToken);
}
if (compatibility) {
// TODO: Remove this case soon
console.log(`Breaking change in the next release: Please use one of the following arguments: '--connection-token', '--connection-token-file' or '--without-connection-token'.`);
return new OptionalServerConnectionToken(await defaultValue());
}
return new MandatoryServerConnectionToken(await defaultValue());
}
export async function determineServerConnectionToken(args: ServerParsedArgs): Promise<ServerConnectionToken | ServerConnectionTokenParseError> {
const readOrGenerateConnectionToken = async () => {
if (!args['user-data-dir']) {
// No place to store it!
return generateUuid();
}
const storageLocation = path.join(args['user-data-dir'], 'token');
// First try to find a connection token
try {
const fileContents = await Promises.readFile(storageLocation);
const connectionToken = fileContents.toString().replace(/\r?\n$/, '');
if (connectionTokenRegex.test(connectionToken)) {
return connectionToken;
}
} catch (err) { }
// No connection token found, generate one
const connectionToken = generateUuid();
try {
// Try to store it
await Promises.writeFile(storageLocation, connectionToken, { mode: 0o600 });
} catch (err) { }
return connectionToken;
};
return parseServerConnectionToken(args, readOrGenerateConnectionToken);
}
export function requestHasValidConnectionToken(connectionToken: ServerConnectionToken, req: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery) {
// First check if there is a valid query parameter
if (connectionToken.validate(parsedUrl.query[connectionTokenQueryName])) {
return true;
}
// Otherwise, check if there is a valid cookie
const cookies = cookie.parse(req.headers.cookie || '');
return connectionToken.validate(cookies[connectionTokenCookieName]);
}

View File

@@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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> = {
/* ----- server setup ----- */
'host': { type: 'string', cat: 'o', args: 'ip-address', description: nls.localize('host', "The host name or IP address the server should listen to. If not set, defaults to 'localhost'.") },
'port': { type: 'string', cat: 'o', args: 'port | port range', description: nls.localize('port', "The port the server should listen to. If 0 is passed a random free port is picked. If a range in the format num-num is passed, a free port from the range (end inclusive) is selected.") },
'pick-port': { type: 'string', deprecationMessage: 'Use the range notation in \'port\' instead.' },
'socket-path': { type: 'string', cat: 'o', args: 'path', description: nls.localize('socket-path', "The path to a socket file for the server to listen to.") },
'connection-token': { type: 'string', cat: 'o', args: 'token', deprecates: ['connectionToken'], description: nls.localize('connection-token', "A secret that must be included with all requests.") },
'connection-token-file': { type: 'string', cat: 'o', args: 'path', deprecates: ['connection-secret', 'connectionTokenFile'], description: nls.localize('connection-token-file', "Path to a file that contains the connection token.") },
'without-connection-token': { type: 'boolean', cat: 'o', description: nls.localize('without-connection-token', "Run without a connection token. Only use this if the connection is secured by other means.") },
'disable-websocket-compression': { type: 'boolean' },
'print-startup-performance': { type: 'boolean' },
'print-ip-address': { type: 'boolean' },
'accept-server-license-terms': { type: 'boolean', cat: 'o', description: nls.localize('acceptLicenseTerms', "If set, the user accepts the server license terms and the server will be started without a user prompt.") },
'server-data-dir': { type: 'string', cat: 'o', description: nls.localize('serverDataDir', "Specifies the directory that server data is kept in.") },
'telemetry-level': { type: 'string', cat: 'o', args: 'level', description: nls.localize('telemetry-level', "Sets the initial telemetry level. Valid levels are: 'off', 'crash', 'error' and 'all'. If not specified, the server will send telemetry until a client connects, it will then use the clients telemetry setting. Setting this to 'off' is equivalent to --disable-telemetry") },
/* ----- vs code options --- -- */
'user-data-dir': OPTIONS['user-data-dir'],
'enable-smoke-test-driver': OPTIONS['enable-smoke-test-driver'],
'disable-telemetry': OPTIONS['disable-telemetry'],
'disable-workspace-trust': OPTIONS['disable-workspace-trust'],
'file-watcher-polling': { type: 'string', deprecates: ['fileWatcherPolling'] },
'log': OPTIONS['log'],
'logsPath': OPTIONS['logsPath'],
'force-disable-user-env': OPTIONS['force-disable-user-env'],
/* ----- vs code web options ----- */
'folder': { type: 'string', deprecationMessage: 'No longer supported. Folder needs to be provided in the browser URL or with `default-folder`.' },
'workspace': { type: 'string', deprecationMessage: 'No longer supported. Workspace needs to be provided in the browser URL or with `default-workspace`.' },
'default-folder': { type: 'string', description: nls.localize('default-folder', 'The workspace folder to open when no input is specified in the browser URL. A relative or absolute path resolved against the current working directory.') },
'default-workspace': { type: 'string', description: nls.localize('default-workspace', 'The workspace to open when no input is specified in the browser URL. A relative or absolute path resolved against the current working directory.') },
'enable-sync': { type: 'boolean' },
'github-auth': { type: 'string' },
/* ----- extension management ----- */
'extensions-dir': OPTIONS['extensions-dir'],
'extensions-download-dir': OPTIONS['extensions-download-dir'],
'builtin-extensions-dir': OPTIONS['builtin-extensions-dir'],
'install-extension': OPTIONS['install-extension'],
'install-builtin-extension': OPTIONS['install-builtin-extension'],
'uninstall-extension': OPTIONS['uninstall-extension'],
'list-extensions': OPTIONS['list-extensions'],
'locate-extension': OPTIONS['locate-extension'],
'show-versions': OPTIONS['show-versions'],
'category': OPTIONS['category'],
'force': OPTIONS['force'],
'do-not-sync': OPTIONS['do-not-sync'],
'pre-release': OPTIONS['pre-release'],
'start-server': { type: 'boolean', cat: 'e', description: nls.localize('start-server', "Start the server when installing or uninstalling extensions. To be used in combination with 'install-extension', 'install-builtin-extension' and 'uninstall-extension'.") },
/* ----- remote development options ----- */
'enable-remote-auto-shutdown': { type: 'boolean' },
'remote-auto-shutdown-without-delay': { type: 'boolean' },
'use-host-proxy': { type: 'boolean' },
'without-browser-env-var': { type: 'boolean' },
/* ----- server cli ----- */
'help': OPTIONS['help'],
'version': OPTIONS['version'],
'compatibility': { type: 'string' },
_: OPTIONS['_']
};
export interface ServerParsedArgs {
/* ----- server setup ----- */
host?: string;
port?: string;
'pick-port'?: string;
'socket-path'?: string;
/**
* A secret token that must be provided by the web client with all requests.
* Use only `[0-9A-Za-z\-]`.
*
* By default, a UUID will be generated every time the server starts up.
*
* If the server is running on a multi-user system, then consider
* using `--connection-token-file` which has the advantage that the token cannot
* be seen by other users using `ps` or similar commands.
*/
'connection-token'?: 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 connection token. 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-token-file'?: string;
/**
* Run the server without a connection token
*/
'without-connection-token'?: boolean;
'disable-websocket-compression'?: boolean;
'print-startup-performance'?: boolean;
'print-ip-address'?: boolean;
'accept-server-license-terms': boolean;
'server-data-dir'?: string;
'telemetry-level'?: string;
'disable-workspace-trust'?: boolean;
/* ----- vs code options ----- */
'user-data-dir'?: string;
'enable-smoke-test-driver'?: boolean;
'disable-telemetry'?: boolean;
'file-watcher-polling'?: string;
'log'?: string;
'logsPath'?: string;
'force-disable-user-env'?: boolean;
/* ----- vs code web options ----- */
'default-workspace'?: string;
'default-folder'?: string;
/** @deprecated, use default-workspace instead */
workspace: string;
/** @deprecated, use default-folder instead */
folder: string;
'enable-sync'?: boolean;
'github-auth'?: string;
/* ----- extension management ----- */
'extensions-dir'?: string;
'extensions-download-dir'?: string;
'builtin-extensions-dir'?: string;
'install-extension'?: string[];
'install-builtin-extension'?: string[];
'uninstall-extension'?: string[];
'list-extensions'?: boolean;
'locate-extension'?: string[];
'show-versions'?: boolean;
'category'?: string;
force?: boolean; // used by install-extension
'do-not-sync'?: boolean; // used by install-extension
'pre-release'?: boolean; // used by install-extension
'start-server'?: boolean;
/* ----- remote development options ----- */
'enable-remote-auto-shutdown'?: boolean;
'remote-auto-shutdown-without-delay'?: boolean;
'use-host-proxy'?: boolean;
'without-browser-env-var'?: boolean;
/* ----- server cli ----- */
help: boolean;
version: boolean;
compatibility: string;
_: string[];
}
export const IServerEnvironmentService = refineServiceDecorator<IEnvironmentService, IServerEnvironmentService>(IEnvironmentService);
export interface IServerEnvironmentService extends INativeEnvironmentService {
readonly args: ServerParsedArgs;
}
export class ServerEnvironmentService extends NativeEnvironmentService implements IServerEnvironmentService {
override get args(): ServerParsedArgs { return super.args as ServerParsedArgs; }
}

View File

@@ -0,0 +1,345 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { hostname, release } from 'os';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import * as path from 'vs/base/common/path';
import { IURITransformer } from 'vs/base/common/uriIpc';
import { getMachineId } from 'vs/base/node/id';
import { Promises } from 'vs/base/node/pfs';
import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, ProxyChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc';
import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials';
import { CredentialsWebMainService } from 'vs/platform/credentials/node/credentialsMainService';
import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc';
import { IDownloadService } from 'vs/platform/download/common/download';
import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc';
import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService';
import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService';
import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { IFileService } from 'vs/platform/files/common/files';
import { FileService } from 'vs/platform/files/common/fileService';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
import { AbstractLogger, DEFAULT_LOG_LEVEL, getLogLevel, ILogService, LogLevel, LogService, MultiplexLogService } from 'vs/platform/log/common/log';
import { LogLevelChannel } from 'vs/platform/log/common/logIpc';
import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IRequestService } from 'vs/platform/request/common/request';
import { RequestChannel } from 'vs/platform/request/common/requestIpc';
import { RequestService } from 'vs/platform/request/node/requestService';
import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties';
import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
import { getPiiPathsFromEnvironment, ITelemetryAppender, NullAppender, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
import ErrorTelemetry from 'vs/platform/telemetry/node/errorTelemetry';
import { IPtyService, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService';
import { RemoteAgentEnvironmentChannel } from 'vs/server/node/remoteAgentEnvironmentImpl';
import { RemoteAgentFileSystemProviderChannel } from 'vs/server/node/remoteFileSystemProviderServer';
import { ServerTelemetryChannel } from 'vs/platform/telemetry/common/remoteTelemetryChannel';
import { IServerTelemetryService, ServerNullTelemetryService, ServerTelemetryService } from 'vs/platform/telemetry/common/serverTelemetryService';
import { RemoteTerminalChannel } from 'vs/server/node/remoteTerminalChannel';
import { createURITransformer } from 'vs/workbench/api/node/uriTransformer';
import { ServerConnectionToken } from 'vs/server/node/serverConnectionToken';
import { ServerEnvironmentService, ServerParsedArgs } from 'vs/server/node/serverEnvironmentService';
import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel';
import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService';
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteFileSystemProviderClient';
import { ExtensionHostStatusService, IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService';
import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService';
import { ExtensionsScannerService } from 'vs/server/node/extensionsScannerService';
const eventPrefix = 'monacoworkbench';
export async function setupServerServices(connectionToken: ServerConnectionToken, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string, disposables: DisposableStore) {
const services = new ServiceCollection();
const socketServer = new SocketServer<RemoteAgentConnectionContext>();
const productService: IProductService = { _serviceBrand: undefined, ...product };
services.set(IProductService, productService);
const environmentService = new ServerEnvironmentService(args, productService);
services.set(IEnvironmentService, environmentService);
services.set(INativeEnvironmentService, environmentService);
const spdLogService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, path.join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, false, getLogLevel(environmentService)));
const logService = new MultiplexLogService([new ServerLogService(getLogLevel(environmentService)), spdLogService]);
services.set(ILogService, logService);
setTimeout(() => cleanupOlderLogs(environmentService.logsPath).then(null, err => logService.error(err)), 10000);
logService.trace(`Remote configuration data at ${REMOTE_DATA_FOLDER}`);
logService.trace('process arguments:', environmentService.args);
if (Array.isArray(productService.serverGreeting)) {
spdLogService.info(`\n\n${productService.serverGreeting.join('\n')}\n\n`);
}
// ExtensionHost Debug broadcast service
socketServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());
// TODO: @Sandy @Joao need dynamic context based router
const router = new StaticRouter<RemoteAgentConnectionContext>(ctx => ctx.clientId === 'renderer');
socketServer.registerChannel('logger', new LogLevelChannel(logService));
// Files
const fileService = disposables.add(new FileService(logService));
services.set(IFileService, fileService);
fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService)));
const configurationService = new ConfigurationService(environmentService.machineSettingsResource, fileService);
services.set(IConfigurationService, configurationService);
const extensionHostStatusService = new ExtensionHostStatusService();
services.set(IExtensionHostStatusService, extensionHostStatusService);
// URI Identity
services.set(IUriIdentityService, new UriIdentityService(fileService));
// Request
services.set(IRequestService, new SyncDescriptor(RequestService));
let appInsightsAppender: ITelemetryAppender = NullAppender;
const machineId = await getMachineId();
if (supportsTelemetry(productService, environmentService)) {
if (productService.aiConfig && productService.aiConfig.asimovKey) {
appInsightsAppender = new AppInsightsAppender(eventPrefix, null, productService.aiConfig.asimovKey);
disposables.add(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data
}
const config: ITelemetryServiceConfig = {
appenders: [appInsightsAppender],
commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, productService.msftInternalDomains, environmentService.installSourcePath, 'remoteAgent'),
piiPaths: getPiiPathsFromEnvironment(environmentService)
};
const initialTelemetryLevelArg = environmentService.args['telemetry-level'];
let injectedTelemetryLevel: TelemetryLevel = TelemetryLevel.USAGE;
// Convert the passed in CLI argument into a telemetry level for the telemetry service
if (initialTelemetryLevelArg === 'all') {
injectedTelemetryLevel = TelemetryLevel.USAGE;
} else if (initialTelemetryLevelArg === 'error') {
injectedTelemetryLevel = TelemetryLevel.ERROR;
} else if (initialTelemetryLevelArg === 'crash') {
injectedTelemetryLevel = TelemetryLevel.CRASH;
} else if (initialTelemetryLevelArg !== undefined) {
injectedTelemetryLevel = TelemetryLevel.NONE;
}
services.set(IServerTelemetryService, new SyncDescriptor(ServerTelemetryService, [config, injectedTelemetryLevel]));
} else {
services.set(IServerTelemetryService, ServerNullTelemetryService);
}
services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService));
const downloadChannel = socketServer.getChannel('download', router);
services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService));
services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
const instantiationService: IInstantiationService = new InstantiationService(services);
services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService));
const extensionManagementCLIService = instantiationService.createInstance(ExtensionManagementCLIService);
services.set(IExtensionManagementCLIService, extensionManagementCLIService);
const ptyService = instantiationService.createInstance(
PtyHostService,
{
graceTime: ProtocolConstants.ReconnectionGraceTime,
shortGraceTime: ProtocolConstants.ReconnectionShortGraceTime,
scrollback: configurationService.getValue<number>(TerminalSettingId.PersistentSessionScrollback) ?? 100
}
);
services.set(IPtyService, ptyService);
services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId]));
services.set(ICredentialsMainService, new SyncDescriptor(CredentialsWebMainService));
instantiationService.invokeFunction(accessor => {
const extensionManagementService = accessor.get(IExtensionManagementService);
const extensionsScannerService = accessor.get(IExtensionsScannerService);
const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, logService, extensionHostStatusService, extensionsScannerService);
socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel);
const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), appInsightsAppender);
socketServer.registerChannel('telemetry', telemetryChannel);
socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyService, productService, extensionManagementService));
const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(logService, environmentService);
socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel);
socketServer.registerChannel('request', new RequestChannel(accessor.get(IRequestService)));
const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority));
socketServer.registerChannel('extensions', channel);
const encryptionChannel = ProxyChannel.fromService<RemoteAgentConnectionContext>(accessor.get(IEncryptionMainService));
socketServer.registerChannel('encryption', encryptionChannel);
const credentialsChannel = ProxyChannel.fromService<RemoteAgentConnectionContext>(accessor.get(ICredentialsMainService));
socketServer.registerChannel('credentials', credentialsChannel);
// clean up deprecated extensions
(extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions();
disposables.add(new ErrorTelemetry(accessor.get(ITelemetryService)));
return {
telemetryService: accessor.get(ITelemetryService)
};
});
return { socketServer, instantiationService };
}
const _uriTransformerCache: { [remoteAuthority: string]: IURITransformer } = Object.create(null);
function getUriTransformer(remoteAuthority: string): IURITransformer {
if (!_uriTransformerCache[remoteAuthority]) {
_uriTransformerCache[remoteAuthority] = createURITransformer(remoteAuthority);
}
return _uriTransformerCache[remoteAuthority];
}
export class SocketServer<TContext = string> extends IPCServer<TContext> {
private _onDidConnectEmitter: Emitter<ClientConnectionEvent>;
constructor() {
const emitter = new Emitter<ClientConnectionEvent>();
super(emitter.event);
this._onDidConnectEmitter = emitter;
}
public acceptConnection(protocol: IMessagePassingProtocol, onDidClientDisconnect: Event<void>): void {
this._onDidConnectEmitter.fire({ protocol, onDidClientDisconnect });
}
}
class ServerLogService extends AbstractLogger implements ILogService {
_serviceBrand: undefined;
private useColors: boolean;
constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) {
super();
this.setLevel(logLevel);
this.useColors = Boolean(process.stdout.isTTY);
}
trace(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Trace) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.log(`[${now()}]`, message, ...args);
}
}
}
debug(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Debug) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.log(`[${now()}]`, message, ...args);
}
}
}
info(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Info) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.log(`[${now()}]`, message, ...args);
}
}
}
warn(message: string | Error, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Warning) {
if (this.useColors) {
console.warn(`\x1b[93m[${now()}]\x1b[0m`, message, ...args);
} else {
console.warn(`[${now()}]`, message, ...args);
}
}
}
error(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Error) {
if (this.useColors) {
console.error(`\x1b[91m[${now()}]\x1b[0m`, message, ...args);
} else {
console.error(`[${now()}]`, message, ...args);
}
}
}
critical(message: string, ...args: any[]): void {
if (this.getLevel() <= LogLevel.Critical) {
if (this.useColors) {
console.error(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
console.error(`[${now()}]`, message, ...args);
}
}
}
override dispose(): void {
// noop
}
flush(): void {
// noop
}
}
function now(): string {
const date = new Date();
return `${twodigits(date.getHours())}:${twodigits(date.getMinutes())}:${twodigits(date.getSeconds())}`;
}
function twodigits(n: number): string {
if (n < 10) {
return `0${n}`;
}
return String(n);
}
/**
* Cleans up older logs, while keeping the 10 most recent ones.
*/
async function cleanupOlderLogs(logsPath: string): Promise<void> {
const currentLog = path.basename(logsPath);
const logsRoot = path.dirname(logsPath);
const children = await Promises.readdir(logsRoot);
const allSessions = children.filter(name => /^\d{8}T\d{6}$/.test(name));
const oldSessions = allSessions.sort().filter((d) => d !== currentLog);
const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9));
await Promise.all(toDelete.map(name => Promises.rm(path.join(logsRoot, name))));
}

View File

@@ -0,0 +1,412 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises as fsp, createReadStream } from 'fs';
import * as path from 'path';
import * as http from 'http';
import * as url from 'url';
import * as cookie from 'cookie';
import * as crypto from 'crypto';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { getMediaMime } from 'vs/base/common/mime';
import { isLinux } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
import { extname, dirname, join, normalize } from 'vs/base/common/path';
import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schemas } from 'vs/base/common/network';
import { generateUuid } from 'vs/base/common/uuid';
import { IProductService } from 'vs/platform/product/common/productService';
import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken';
import { asTextOrError, IRequestService } from 'vs/platform/request/common/request';
import { IHeaders } from 'vs/base/parts/request/common/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { streamToBuffer } from 'vs/base/common/buffer';
import { IProductConfiguration } from 'vs/base/common/product';
import { isString } from 'vs/base/common/types';
import { CharCode } from 'vs/base/common/charCode';
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);
}
export const enum CacheControl {
NO_CACHING, ETAG, NO_EXPIRY
}
/**
* Serve a file at a given path or 404 if the file is missing.
*/
export async function serveFile(filePath: string, cacheControl: CacheControl, logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, responseHeaders: Record<string, string>): Promise<any> {
try {
const stat = await fsp.stat(filePath); // throws an error if file doesn't exist
if (cacheControl === CacheControl.ETAG) {
// 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();
}
responseHeaders['Etag'] = etag;
} else if (cacheControl === CacheControl.NO_EXPIRY) {
responseHeaders['Cache-Control'] = 'public, max-age=31536000';
} else if (cacheControl === CacheControl.NO_CACHING) {
responseHeaders['Cache-Control'] = 'no-store';
}
responseHeaders['Content-Type'] = textMimeType[extname(filePath)] || getMediaMime(filePath) || 'text/plain';
res.writeHead(200, responseHeaders);
// Data
createReadStream(filePath).pipe(res);
} catch (error) {
if (error.code !== 'ENOENT') {
logService.error(error);
console.error(error.toString());
} else {
console.error(`File not found: ${filePath}`);
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
return res.end('Not found');
}
}
const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath);
export class WebClientServer {
private readonly _webExtensionResourceUrlTemplate: URI | undefined;
private readonly _staticRoute: string;
private readonly _callbackRoute: string;
constructor(
private readonly _connectionToken: ServerConnectionToken,
@IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService,
@ILogService private readonly _logService: ILogService,
@IRequestService private readonly _requestService: IRequestService,
@IProductService private readonly _productService: IProductService,
) {
this._webExtensionResourceUrlTemplate = this._productService.extensionsGallery?.resourceUrlTemplate ? URI.parse(this._productService.extensionsGallery.resourceUrlTemplate) : undefined;
const qualityAndCommit = `${_productService.quality ?? 'oss'}-${_productService.commit ?? 'dev'}`;
this._staticRoute = `/${qualityAndCommit}/static`;
this._callbackRoute = `/${qualityAndCommit}/callback`;
}
/**
* Handle web resources (i.e. only needed by the web client).
* **NOTE**: This method is only invoked when the server has web bits.
* **NOTE**: This method is only invoked after the connection token has been validated.
*/
async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
try {
const pathname = parsedUrl.pathname!;
if (pathname.startsWith(this._staticRoute) && pathname.charCodeAt(this._staticRoute.length) === CharCode.Slash) {
return this._handleStatic(req, res, parsedUrl);
}
if (pathname === '/') {
return this._handleRoot(req, res, parsedUrl);
}
if (pathname === this._callbackRoute) {
// callback support
return this._handleCallback(res);
}
if (/^\/web-extension-resource\//.test(pathname)) {
// extension resource support
return this._handleWebExtensionResource(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.');
}
}
/**
* 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 the this._staticRoute from the path
const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20)
const relativeFilePath = normalizedPathname.substring(this._staticRoute.length + 1);
const filePath = join(APP_ROOT, relativeFilePath); // join also normalizes the path
if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) {
return serveError(req, res, 400, `Bad request.`);
}
return serveFile(filePath, this._environmentService.isBuilt ? CacheControl.NO_EXPIRY : CacheControl.ETAG, this._logService, req, res, headers);
}
private _getResourceURLTemplateAuthority(uri: URI): string | undefined {
const index = uri.authority.indexOf('.');
return index !== -1 ? uri.authority.substring(index + 1) : undefined;
}
/**
* Handle extension resources
*/
private async _handleWebExtensionResource(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<any> {
if (!this._webExtensionResourceUrlTemplate) {
return serveError(req, res, 500, 'No extension gallery service configured.');
}
// Strip `/web-extension-resource/` from the path
const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20)
const path = normalize(normalizedPathname.substr('/web-extension-resource/'.length));
const uri = URI.parse(path).with({
scheme: this._webExtensionResourceUrlTemplate.scheme,
authority: path.substring(0, path.indexOf('/')),
path: path.substring(path.indexOf('/') + 1)
});
if (this._getResourceURLTemplateAuthority(this._webExtensionResourceUrlTemplate) !== this._getResourceURLTemplateAuthority(uri)) {
return serveError(req, res, 403, 'Request Forbidden');
}
const headers: IHeaders = {};
const setRequestHeader = (header: string) => {
const value = req.headers[header];
if (value && (isString(value) || value[0])) {
headers[header] = isString(value) ? value : value[0];
} else if (header !== header.toLowerCase()) {
setRequestHeader(header.toLowerCase());
}
};
setRequestHeader('X-Client-Name');
setRequestHeader('X-Client-Version');
setRequestHeader('X-Machine-Id');
setRequestHeader('X-Client-Commit');
const context = await this._requestService.request({
type: 'GET',
url: uri.toString(true),
headers
}, CancellationToken.None);
const status = context.res.statusCode || 500;
if (status !== 200) {
let text: string | null = null;
try {
text = await asTextOrError(context);
} catch (error) {/* Ignore */ }
return serveError(req, res, status, text || `Request failed with status ${status}`);
}
const responseHeaders: Record<string, string> = Object.create(null);
const setResponseHeader = (header: string) => {
const value = context.res.headers[header];
if (value) {
responseHeaders[header] = value;
} else if (header !== header.toLowerCase()) {
setResponseHeader(header.toLowerCase());
}
};
setResponseHeader('Cache-Control');
setResponseHeader('Content-Type');
res.writeHead(200, responseHeaders);
const buffer = await streamToBuffer(context.stream);
return res.end(buffer.buffer);
}
/**
* Handle HTTP requests for /
*/
private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<any> {
const queryConnectionToken = parsedUrl.query[connectionTokenQueryName];
if (typeof queryConnectionToken === 'string') {
// We got a connection token as a query parameter.
// We want to have a clean URL, so we strip it
const responseHeaders: Record<string, string> = Object.create(null);
responseHeaders['Set-Cookie'] = cookie.serialize(
connectionTokenCookieName,
queryConnectionToken,
{
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 /* 1 week */
}
);
const newQuery = Object.create(null);
for (let key in parsedUrl.query) {
if (key !== connectionTokenQueryName) {
newQuery[key] = parsedUrl.query[key];
}
}
const newLocation = url.format({ pathname: '/', query: newQuery });
responseHeaders['Location'] = newLocation;
res.writeHead(302, responseHeaders);
return res.end();
}
let originalHost = req.headers['x-original-host'];
if (Array.isArray(originalHost)) {
originalHost = originalHost[0];
}
const remoteAuthority = originalHost || req.headers.host;
if (!remoteAuthority) {
return serveError(req, res, 400, `Bad request.`);
}
function asJSON(value: unknown): string {
return JSON.stringify(value).replace(/"/g, '&quot;');
}
let _wrapWebWorkerExtHostInIframe: undefined | false = undefined;
if (this._environmentService.args['enable-smoke-test-driver']) {
// 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 resolveWorkspaceURI = (defaultLocation?: string) => defaultLocation && URI.file(path.resolve(defaultLocation)).with({ scheme: Schemas.vscodeRemote, authority: remoteAuthority });
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 workbenchWebConfiguration = {
remoteAuthority,
_wrapWebWorkerExtHostInIframe,
developmentOptions: { enableSmokeTestDriver: this._environmentService.args['enable-smoke-test-driver'] ? true : undefined },
settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined,
enableWorkspaceTrust: !this._environmentService.args['disable-workspace-trust'],
folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']),
workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']),
productConfiguration: <Partial<IProductConfiguration>>{
embedderIdentifier: 'server-distro',
extensionsGallery: this._webExtensionResourceUrlTemplate ? {
...this._productService.extensionsGallery,
'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({
scheme: 'http',
authority: remoteAuthority,
path: `web-extension-resource/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}`
}).toString(true)
} : undefined
},
callbackRoute: this._callbackRoute
};
const values: { [key: string]: string } = {
WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration),
WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '',
WORKBENCH_WEB_BASE_URL: this._staticRoute,
};
let data;
try {
const workbenchTemplate = (await fsp.readFile(filePath)).toString();
data = workbenchTemplate.replace(/\{\{([^}]+)\}\}/g, (_, key) => values[key] ?? 'undefined');
} catch (e) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
return res.end('Not found');
}
const cspDirectives = [
'default-src \'self\';',
'img-src \'self\' https: data: blob:;',
'media-src \'self\';',
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
'child-src \'self\';',
`frame-src 'self' https://*.vscode-cdn.net data:;`,
'worker-src \'self\' data:;',
'style-src \'self\' \'unsafe-inline\';',
'connect-src \'self\' ws: wss: https:;',
'font-src \'self\' blob:;',
'manifest-src \'self\';'
].join(' ');
const headers: http.OutgoingHttpHeaders = {
'Content-Type': 'text/html',
'Content-Security-Policy': cspDirectives
};
if (this._connectionToken.type !== ServerConnectionTokenType.None) {
// 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
headers['Set-Cookie'] = cookie.serialize(
connectionTokenCookieName,
this._connectionToken.value,
{
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 /* 1 week */
}
);
}
res.writeHead(200, headers);
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;
}
/**
* Handle HTTP requests for /callback
*/
private async _handleCallback(res: http.ServerResponse): Promise<any> {
const filePath = FileAccess.asFileUri('vs/code/browser/workbench/callback.html', require).fsPath;
const data = (await fsp.readFile(filePath)).toString();
const cspDirectives = [
'default-src \'self\';',
'img-src \'self\' https: data: blob:;',
'media-src \'none\';',
`script-src 'self' ${this._getScriptCspHashes(data).join(' ')};`,
'style-src \'self\' \'unsafe-inline\';',
'font-src \'self\' blob:;'
].join(' ');
res.writeHead(200, {
'Content-Type': 'text/html',
'Content-Security-Policy': cspDirectives
});
return res.end(data);
}
}

View File

@@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { startExtensionHostProcess } from 'vs/workbench/services/extensions/node/extensionHostProcessSetup';
startExtensionHostProcess().catch((err) => console.log(err));

View File

@@ -1,64 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings';
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
import { NullTelemetryServiceShape } from 'vs/platform/telemetry/common/telemetryUtils';
export interface IRemoteTelemetryService extends ITelemetryService {
permanentlyDisableTelemetry(): void
}
export class RemoteTelemetryService extends TelemetryService implements IRemoteTelemetryService {
private _isDisabled = false;
constructor(
config: ITelemetryServiceConfig,
@IConfigurationService _configurationService: IConfigurationService
) {
super(config, _configurationService);
}
override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLog(eventName, data, anonymizeFilePaths);
}
override publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>, anonymizeFilePaths?: boolean): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLog2(eventName, data, anonymizeFilePaths);
}
override publicLogError(errorEventName: string, data?: ITelemetryData): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLogError(errorEventName, data);
}
override publicLogError2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): Promise<void> {
if (this._isDisabled) {
return Promise.resolve(undefined);
}
return super.publicLogError2(eventName, data);
}
permanentlyDisableTelemetry(): void {
this._isDisabled = true;
this.dispose();
}
}
export const RemoteNullTelemetryService = new class extends NullTelemetryServiceShape implements IRemoteTelemetryService {
permanentlyDisableTelemetry(): void { return; } // No-op, telemetry is already disabled
};
export const IRemoteTelemetryService = refineServiceDecorator<ITelemetryService, IRemoteTelemetryService>(ITelemetryService);

View File

@@ -1,15 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URITransformer, IURITransformer, IRawURITransformer } from 'vs/base/common/uriIpc';
import { FileAccess } from 'vs/base/common/network';
export const uriTransformerPath = FileAccess.asFileUri('vs/server/uriTransformer.js', require).fsPath;
export function createRemoteURITransformer(remoteAuthority: string): IURITransformer {
const rawURITransformerFactory = <any>require.__$__nodeRequire(uriTransformerPath);
const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
return new URITransformer(rawURITransformer);
}

View File

@@ -1,126 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { OPTIONS, OptionDescriptions } from 'vs/platform/environment/node/argv';
import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
export const serverOptions: OptionDescriptions<ServerParsedArgs> = {
'port': { type: 'string' },
'connectionToken': { type: 'string' },
'connection-secret': { type: 'string', description: nls.localize('connection-secret', "Path to file that contains the connection token. This will require that all incoming connections know the secret.") },
'host': { type: 'string' },
'socket-path': { type: 'string' },
'driver': { type: 'string' },
'start-server': { type: 'boolean' },
'print-startup-performance': { type: 'boolean' },
'print-ip-address': { type: 'boolean' },
'disable-websocket-compression': { type: 'boolean' },
'fileWatcherPolling': { type: 'string' },
'enable-remote-auto-shutdown': { type: 'boolean' },
'remote-auto-shutdown-without-delay': { type: 'boolean' },
'without-browser-env-var': { type: 'boolean' },
'disable-telemetry': OPTIONS['disable-telemetry'],
'extensions-dir': OPTIONS['extensions-dir'],
'extensions-download-dir': OPTIONS['extensions-download-dir'],
'install-extension': OPTIONS['install-extension'],
'install-builtin-extension': OPTIONS['install-builtin-extension'],
'uninstall-extension': OPTIONS['uninstall-extension'],
'locate-extension': OPTIONS['locate-extension'],
'list-extensions': OPTIONS['list-extensions'],
'force': OPTIONS['force'],
'show-versions': OPTIONS['show-versions'],
'category': OPTIONS['category'],
'do-not-sync': OPTIONS['do-not-sync'],
'force-disable-user-env': OPTIONS['force-disable-user-env'],
'folder': { type: 'string' },
'workspace': { type: 'string' },
'web-user-data-dir': { type: 'string' },
'use-host-proxy': { type: 'string' },
'enable-sync': { type: 'boolean' },
'github-auth': { type: 'string' },
'log': { type: 'string' },
'logsPath': { type: 'string' },
_: OPTIONS['_']
};
export interface ServerParsedArgs {
port?: string;
connectionToken?: string;
/**
* A path to a filename which will be read on startup.
* Consider placing this file in a folder readable only by the same user (a `chmod 0700` directory).
*
* The contents of the file will be used as the connectionToken. Use only `[0-9A-Z\-]` as contents in the file.
* The file can optionally end in a `\n` which will be ignored.
*
* This secret must be communicated to any vscode instance via the resolver or embedder API.
*/
'connection-secret'?: string;
host?: string;
'socket-path'?: string;
driver?: string;
'print-startup-performance'?: boolean;
'print-ip-address'?: boolean;
'disable-websocket-compression'?: boolean;
'disable-telemetry'?: boolean;
fileWatcherPolling?: string;
'start-server'?: boolean;
'enable-remote-auto-shutdown'?: boolean;
'remote-auto-shutdown-without-delay'?: boolean;
'extensions-dir'?: string;
'extensions-download-dir'?: string;
'install-extension'?: string[];
'install-builtin-extension'?: string[];
'uninstall-extension'?: string[];
'list-extensions'?: boolean;
'locate-extension'?: string[];
'show-versions'?: boolean;
'category'?: string;
'force-disable-user-env'?: boolean;
'use-host-proxy'?: string;
'without-browser-env-var'?: boolean;
force?: boolean; // used by install-extension
'do-not-sync'?: boolean; // used by install-extension
'user-data-dir'?: string;
'builtin-extensions-dir'?: string;
// web
workspace: string;
folder: string;
'web-user-data-dir'?: string;
'enable-sync'?: boolean;
'github-auth'?: string;
'log'?: string;
'logsPath'?: string;
_: string[];
}
export const IServerEnvironmentService = refineServiceDecorator<IEnvironmentService, IServerEnvironmentService>(IEnvironmentService);
export interface IServerEnvironmentService extends INativeEnvironmentService {
readonly args: ServerParsedArgs;
}
export class ServerEnvironmentService extends NativeEnvironmentService implements IServerEnvironmentService {
override get args(): ServerParsedArgs { return super.args as ServerParsedArgs; }
}

View File

@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { parseServerConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken';
import { ServerParsedArgs } from 'vs/server/node/serverEnvironmentService';
suite('parseServerConnectionToken', () => {
function isError(r: ServerConnectionToken | ServerConnectionTokenParseError): r is ServerConnectionTokenParseError {
return (r instanceof ServerConnectionTokenParseError);
}
function assertIsError(r: ServerConnectionToken | ServerConnectionTokenParseError): void {
assert.strictEqual(isError(r), true);
}
test('no arguments generates a token that is mandatory', async () => {
const result = await parseServerConnectionToken({} as ServerParsedArgs, async () => 'defaultTokenValue');
assert.ok(!(result instanceof ServerConnectionTokenParseError));
assert.ok(result.type === ServerConnectionTokenType.Mandatory);
});
test('no arguments with --compatibility generates a token that is not mandatory', async () => {
const result = await parseServerConnectionToken({ 'compatibility': '1.63' } as ServerParsedArgs, async () => 'defaultTokenValue');
assert.ok(!(result instanceof ServerConnectionTokenParseError));
assert.ok(result.type === ServerConnectionTokenType.Optional);
assert.strictEqual(result.value, 'defaultTokenValue');
});
test('--without-connection-token', async () => {
const result = await parseServerConnectionToken({ 'without-connection-token': true } as ServerParsedArgs, async () => 'defaultTokenValue');
assert.ok(!(result instanceof ServerConnectionTokenParseError));
assert.ok(result.type === ServerConnectionTokenType.None);
});
test('--without-connection-token --connection-token results in error', async () => {
assertIsError(await parseServerConnectionToken({ 'without-connection-token': true, 'connection-token': '0' } as ServerParsedArgs, async () => 'defaultTokenValue'));
});
test('--without-connection-token --connection-token-file results in error', async () => {
assertIsError(await parseServerConnectionToken({ 'without-connection-token': true, 'connection-token-file': '0' } as ServerParsedArgs, async () => 'defaultTokenValue'));
});
test('--connection-token-file --connection-token results in error', async () => {
assertIsError(await parseServerConnectionToken({ 'connection-token-file': '0', 'connection-token': '0' } as ServerParsedArgs, async () => 'defaultTokenValue'));
});
test('--connection-token-file', async function () {
this.timeout(10000);
const testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'server-connection-token');
fs.mkdirSync(testDir, { recursive: true });
const filename = path.join(testDir, 'connection-token-file');
const connectionToken = `12345-123-abc`;
fs.writeFileSync(filename, connectionToken);
const result = await parseServerConnectionToken({ 'connection-token-file': filename } as ServerParsedArgs, async () => 'defaultTokenValue');
assert.ok(!(result instanceof ServerConnectionTokenParseError));
assert.ok(result.type === ServerConnectionTokenType.Mandatory);
assert.strictEqual(result.value, connectionToken);
fs.rmSync(testDir, { recursive: true, force: true });
});
test('--connection-token', async () => {
const connectionToken = `12345-123-abc`;
const result = await parseServerConnectionToken({ 'connection-token': connectionToken } as ServerParsedArgs, async () => 'defaultTokenValue');
assert.ok(!(result instanceof ServerConnectionTokenParseError));
assert.ok(result.type === ServerConnectionTokenType.Mandatory);
assert.strictEqual(result.value, connectionToken);
});
test('--connection-token --compatibility marks a as not mandatory', async () => {
const connectionToken = `12345-123-abc`;
const result = await parseServerConnectionToken({ 'connection-token': connectionToken, 'compatibility': '1.63' } as ServerParsedArgs, async () => 'defaultTokenValue');
assert.ok(!(result instanceof ServerConnectionTokenParseError));
assert.ok(result.type === ServerConnectionTokenType.Optional);
assert.strictEqual(result.value, connectionToken);
});
});

View File

@@ -1,64 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
/**
* ```
* --------------------------------
* | UI SIDE | AGENT SIDE |
* |---------------|--------------|
* | vscode-remote | file |
* | file | vscode-local |
* --------------------------------
* ```
* @typedef { import('../base/common/uriIpc').IRawURITransformer } IRawURITransformer
* @typedef { import('../base/common/uriIpc').UriParts } UriParts
* @typedef { import('../base/common/uri').UriComponents } UriComponents
* @param {string} remoteAuthority
* @returns {IRawURITransformer}
*/
module.exports = function(remoteAuthority) {
return {
/**
* @param {UriParts} uri
* @returns {UriParts}
*/
transformIncoming: (uri) => {
if (uri.scheme === 'vscode-remote') {
return { scheme: 'file', path: uri.path, query: uri.query, fragment: uri.fragment };
}
if (uri.scheme === 'file') {
return { scheme: 'vscode-local', path: uri.path, query: uri.query, fragment: uri.fragment };
}
return uri;
},
/**
* @param {UriParts} uri
* @returns {UriParts}
*/
transformOutgoing: (uri) => {
if (uri.scheme === 'file') {
return { scheme: 'vscode-remote', authority: remoteAuthority, path: uri.path, query: uri.query, fragment: uri.fragment };
}
if (uri.scheme === 'vscode-local') {
return { scheme: 'file', path: uri.path, query: uri.query, fragment: uri.fragment };
}
return uri;
},
/**
* @param {string} scheme
* @returns {string}
*/
transformOutgoingScheme: (scheme) => {
if (scheme === 'file') {
return 'vscode-remote';
} else if (scheme === 'vscode-local') {
return 'file';
}
return scheme;
}
};
};

View File

@@ -1,354 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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 { 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';
import { IProductService } from 'vs/platform/product/common/productService';
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,
private readonly _productService: IProductService
) { }
async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
try {
const pathname = parsedUrl.pathname!;
if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname === '/code-192.png' || pathname === '/code-512.png') {
// always serve icons/manifest, even without a token
return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', pathname.substr(1)));
}
if (/^\/static\//.test(pathname)) {
// always serve static requests, even without a token
return this._handleStatic(req, res, parsedUrl);
}
if (pathname === '/') {
// the token handling is done inside the handler
return this._handleRoot(req, res, parsedUrl);
}
if (pathname === '/callback') {
// callback support
return this._handleCallback(req, res, parsedUrl);
}
if (pathname === '/fetch-callback') {
// callback fetch support
return this._handleFetchCallback(req, res, parsedUrl);
}
return serveError(req, res, 404, 'Not found.');
} catch (error) {
this._logService.error(error);
console.error(error.toString());
return serveError(req, res, 500, 'Internal Server Error.');
}
}
private _hasCorrectTokenCookie(req: http.IncomingMessage): boolean {
const cookies = cookie.parse(req.headers.cookie || '');
return (cookies['vscode-tkn'] === this._connectionToken);
}
/**
* Handle HTTP requests for /static/*
*/
private async _handleStatic(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
const headers: Record<string, string> = Object.create(null);
// Strip `/static/` from the path
const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20)
const relativeFilePath = normalize(normalizedPathname.substr('/static/'.length));
const filePath = join(APP_ROOT, relativeFilePath);
if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) {
return serveError(req, res, 400, `Bad request.`);
}
return serveFile(this._logService, req, res, filePath, headers);
}
/**
* Handle HTTP requests for /
*/
private async _handleRoot(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
if (!req.headers.host) {
return serveError(req, res, 400, `Bad request.`);
}
const queryTkn = parsedUrl.query['tkn'];
if (typeof queryTkn === 'string') {
// tkn came in via a query string
// => set a cookie and redirect to url without tkn
const responseHeaders: Record<string, string> = Object.create(null);
responseHeaders['Set-Cookie'] = cookie.serialize('vscode-tkn', queryTkn, { sameSite: 'strict', maxAge: 60 * 60 * 24 * 7 /* 1 week */ });
const newQuery = Object.create(null);
for (let key in parsedUrl.query) {
if (key !== 'tkn') {
newQuery[key] = parsedUrl.query[key];
}
}
const newLocation = url.format({ pathname: '/', query: newQuery });
responseHeaders['Location'] = newLocation;
res.writeHead(302, responseHeaders);
return res.end();
}
if (this._environmentService.isBuilt && !this._hasCorrectTokenCookie(req)) {
return serveError(req, res, 403, `Forbidden.`);
}
const remoteAuthority = req.headers.host;
const transformer = createRemoteURITransformer(remoteAuthority);
const { workspacePath, isFolder } = await this._getWorkspaceFromCLI();
function escapeAttribute(value: string): string {
return value.replace(/"/g, '&quot;');
}
let _wrapWebWorkerExtHostInIframe: undefined | false = undefined;
if (this._environmentService.driverHandle) {
// integration tests run at a time when the built output is not yet published to the CDN
// so we must disable the iframe wrapping because the iframe URL will give a 404
_wrapWebWorkerExtHostInIframe = false;
}
const filePath = FileAccess.asFileUri(this._environmentService.isBuilt ? 'vs/code/browser/workbench/workbench.html' : 'vs/code/browser/workbench/workbench-dev.html', require).fsPath;
const authSessionInfo = !this._environmentService.isBuilt && this._environmentService.args['github-auth'] ? {
id: generateUuid(),
providerId: 'github',
accessToken: this._environmentService.args['github-auth'],
scopes: [['user:email'], ['repo']]
} : undefined;
const data = (await util.promisify(fs.readFile)(filePath)).toString()
.replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({
folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined,
workspaceUri: (workspacePath && !isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined,
remoteAuthority,
_wrapWebWorkerExtHostInIframe,
developmentOptions: { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined },
settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined,
})))
.replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '');
const cspDirectives = [
'default-src \'self\';',
'img-src \'self\' https: data: blob:;',
'media-src \'none\';',
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-cb2sg39EJV8ABaSNFfWu/ou8o1xVXYK7jp90oZ9vpcg=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html
'child-src \'self\';',
`frame-src 'self' https://*.vscode-webview.net ${this._productService.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 || this._productService.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));
}
}