Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ExtHostOutputService2 } from 'vs/workbench/api/node/extHostOutputService';
import { ExtHostTerminalService } from 'vs/workbench/api/node/extHostTerminalService';
import { ExtHostTask } from 'vs/workbench/api/node/extHostTask';
import { ExtHostDebugService } from 'vs/workbench/api/node/extHostDebugService';
import { NativeExtHostSearch } from 'vs/workbench/api/node/extHostSearch';
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
import { ExtHostTunnelService } from 'vs/workbench/api/node/extHostTunnelService';
import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput';
import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
import { IExtHostTask } from 'vs/workbench/api/common/extHostTask';
import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService';
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
import { ILogService } from 'vs/platform/log/common/log';
// #########################################################################
// ### ###
// ### !!! PLEASE ADD COMMON IMPORTS INTO extHost.common.services.ts !!! ###
// ### ###
// #########################################################################
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
registerSingleton(ILogService, ExtHostLogService);
registerSingleton(IExtHostDebugService, ExtHostDebugService);
registerSingleton(IExtHostOutputService, ExtHostOutputService2);
registerSingleton(IExtHostSearch, NativeExtHostSearch);
registerSingleton(IExtHostTask, ExtHostTask);
registerSingleton(IExtHostTerminalService, ExtHostTerminalService);
registerSingleton(IExtHostTunnelService, ExtHostTunnelService);

View File

@@ -0,0 +1,191 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createRandomIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
import * as http from 'http';
import * as fs from 'fs';
import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/windows';
import { URI } from 'vs/base/common/uri';
import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces';
import { ILogService } from 'vs/platform/log/common/log';
export interface OpenCommandPipeArgs {
type: 'open';
fileURIs?: string[];
folderURIs: string[];
forceNewWindow?: boolean;
diffMode?: boolean;
addMode?: boolean;
gotoLineMode?: boolean;
forceReuseWindow?: boolean;
waitMarkerFilePath?: string;
}
export interface StatusPipeArgs {
type: 'status';
}
export interface RunCommandPipeArgs {
type: 'command';
command: string;
args: any[];
}
export interface ICommandsExecuter {
executeCommand<T>(id: string, ...args: any[]): Promise<T>;
}
export class CLIServerBase {
private readonly _server: http.Server;
constructor(
private readonly _commands: ICommandsExecuter,
private readonly logService: ILogService,
private readonly _ipcHandlePath: string,
) {
this._server = http.createServer((req, res) => this.onRequest(req, res));
this.setup().catch(err => {
logService.error(err);
return '';
});
}
public get ipcHandlePath() {
return this._ipcHandlePath;
}
private async setup(): Promise<string> {
try {
this._server.listen(this.ipcHandlePath);
this._server.on('error', err => this.logService.error(err));
} catch (err) {
this.logService.error('Could not start open from terminal server.');
}
return this._ipcHandlePath;
}
private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
const chunks: string[] = [];
req.setEncoding('utf8');
req.on('data', (d: string) => chunks.push(d));
req.on('end', () => {
const data: OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | any = JSON.parse(chunks.join(''));
switch (data.type) {
case 'open':
this.open(data, res);
break;
case 'status':
this.getStatus(data, res);
break;
case 'command':
this.runCommand(data, res)
.catch(this.logService.error);
break;
default:
res.writeHead(404);
res.write(`Unknown message type: ${data.type}`, err => {
if (err) {
this.logService.error(err);
}
});
res.end();
break;
}
});
}
private open(data: OpenCommandPipeArgs, res: http.ServerResponse) {
let { fileURIs, folderURIs, forceNewWindow, diffMode, addMode, forceReuseWindow, gotoLineMode, waitMarkerFilePath } = data;
const urisToOpen: IWindowOpenable[] = [];
if (Array.isArray(folderURIs)) {
for (const s of folderURIs) {
try {
urisToOpen.push({ folderUri: URI.parse(s) });
} catch (e) {
// ignore
}
}
}
if (Array.isArray(fileURIs)) {
for (const s of fileURIs) {
try {
if (hasWorkspaceFileExtension(s)) {
urisToOpen.push({ workspaceUri: URI.parse(s) });
} else {
urisToOpen.push({ fileUri: URI.parse(s) });
}
} catch (e) {
// ignore
}
}
}
if (urisToOpen.length) {
const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined;
const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode;
const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI };
this._commands.executeCommand('_files.windowOpen', urisToOpen, windowOpenArgs);
}
res.writeHead(200);
res.end();
}
private async getStatus(data: StatusPipeArgs, res: http.ServerResponse) {
try {
const status = await this._commands.executeCommand('_issues.getSystemStatus');
res.writeHead(200);
res.write(status);
res.end();
} catch (err) {
res.writeHead(500);
res.write(String(err), err => {
if (err) {
this.logService.error(err);
}
});
res.end();
}
}
private async runCommand(data: RunCommandPipeArgs, res: http.ServerResponse) {
try {
const { command, args } = data;
const result = await this._commands.executeCommand(command, ...args);
res.writeHead(200);
res.write(JSON.stringify(result), err => {
if (err) {
this.logService.error(err);
}
});
res.end();
} catch (err) {
res.writeHead(500);
res.write(String(err), err => {
if (err) {
this.logService.error(err);
}
});
res.end();
}
}
dispose(): void {
this._server.close();
if (this._ipcHandlePath && process.platform !== 'win32' && fs.existsSync(this._ipcHandlePath)) {
fs.unlinkSync(this._ipcHandlePath);
}
}
}
export class CLIServer extends CLIServerBase {
constructor(
@IExtHostCommands commands: IExtHostCommands,
@ILogService logService: ILogService
) {
super(commands, logService, createRandomIPCHandle());
}
}

View File

@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import type * as vscode from 'vscode';
import * as env from 'vs/base/common/platform';
import { DebugAdapterExecutable } from 'vs/workbench/api/common/extHostTypes';
import { ExecutableDebugAdapter, SocketDebugAdapter, NamedPipeDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter';
import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { IAdapterDescriptor } from 'vs/workbench/contrib/debug/common/debug';
import { IExtHostConfiguration, ExtHostConfigProvider } from '../common/extHostConfiguration';
import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { ExtHostDebugServiceBase, ExtHostDebugSession, ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService';
import { ISignService } from 'vs/platform/sign/common/sign';
import { SignService } from 'vs/platform/sign/node/signService';
import { hasChildProcesses, prepareCommand, runInExternalTerminal } from 'vs/workbench/contrib/debug/node/terminals';
import { IDisposable } from 'vs/base/common/lifecycle';
import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver';
export class ExtHostDebugService extends ExtHostDebugServiceBase {
readonly _serviceBrand: undefined;
private _integratedTerminalInstance?: vscode.Terminal;
private _terminalDisposedListener: IDisposable | undefined;
constructor(
@IExtHostRpcService extHostRpcService: IExtHostRpcService,
@IExtHostWorkspace workspaceService: IExtHostWorkspace,
@IExtHostExtensionService extensionService: IExtHostExtensionService,
@IExtHostDocumentsAndEditors editorsService: IExtHostDocumentsAndEditors,
@IExtHostConfiguration configurationService: IExtHostConfiguration,
@IExtHostTerminalService private _terminalService: IExtHostTerminalService,
@IExtHostCommands commandService: IExtHostCommands
) {
super(extHostRpcService, workspaceService, extensionService, editorsService, configurationService, commandService);
}
protected createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined {
switch (adapter.type) {
case 'server':
return new SocketDebugAdapter(adapter);
case 'pipeServer':
return new NamedPipeDebugAdapter(adapter);
case 'executable':
return new ExecutableDebugAdapter(adapter, session.type);
}
return super.createDebugAdapter(adapter, session);
}
protected daExecutableFromPackage(session: ExtHostDebugSession, extensionRegistry: ExtensionDescriptionRegistry): DebugAdapterExecutable | undefined {
const dae = ExecutableDebugAdapter.platformAdapterExecutable(extensionRegistry.getAllExtensionDescriptions(), session.type);
if (dae) {
return new DebugAdapterExecutable(dae.command, dae.args, dae.options);
}
return undefined;
}
protected createSignService(): ISignService | undefined {
return new SignService();
}
public async $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise<number | undefined> {
if (args.kind === 'integrated') {
if (!this._terminalDisposedListener) {
// React on terminal disposed and check if that is the debug terminal #12956
this._terminalDisposedListener = this._terminalService.onDidCloseTerminal(terminal => {
if (this._integratedTerminalInstance && this._integratedTerminalInstance === terminal) {
this._integratedTerminalInstance = undefined;
}
});
}
let needNewTerminal = true; // be pessimistic
if (this._integratedTerminalInstance) {
const pid = await this._integratedTerminalInstance.processId;
needNewTerminal = await hasChildProcesses(pid); // if no processes running in terminal reuse terminal
}
const configProvider = await this._configurationService.getConfigProvider();
const shell = this._terminalService.getDefaultShell(true, configProvider);
let cwdForPrepareCommand: string | undefined;
if (needNewTerminal || !this._integratedTerminalInstance) {
const options: vscode.TerminalOptions = {
shellPath: shell,
// shellArgs: this._terminalService._getDefaultShellArgs(configProvider),
cwd: args.cwd,
name: args.title || nls.localize('debug.terminal.title', "debuggee"),
};
this._integratedTerminalInstance = this._terminalService.createTerminalFromOptions(options, true);
} else {
cwdForPrepareCommand = args.cwd;
}
const terminal = this._integratedTerminalInstance;
terminal.show();
const shellProcessId = await this._integratedTerminalInstance.processId;
const command = prepareCommand(shell, args.args, cwdForPrepareCommand, args.env);
terminal.sendText(command, true);
return shellProcessId;
} else if (args.kind === 'external') {
return runInExternalTerminal(args, await this._configurationService.getConfigProvider());
}
return super.$runInTerminal(args);
}
protected createVariableResolver(folders: vscode.WorkspaceFolder[], editorService: ExtHostDocumentsAndEditors, configurationService: ExtHostConfigProvider): AbstractVariableResolverService {
return new ExtHostVariableResolverService(folders, editorService, configurationService, process.env as env.IProcessEnvironment);
}
}

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { generateUuid } from 'vs/base/common/uuid';
import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { Disposable } from 'vs/base/common/lifecycle';
import { MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { URI } from 'vs/base/common/uri';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
export class ExtHostDownloadService extends Disposable {
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostCommands commands: IExtHostCommands
) {
super();
const proxy = extHostRpc.getProxy(MainContext.MainThreadDownloadService);
commands.registerCommand(false, '_workbench.downloadResource', async (resource: URI): Promise<any> => {
const location = URI.file(join(tmpdir(), generateUuid()));
await proxy.$download(resource, location);
return location;
});
}
}

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl';
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
import { MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHostExtensionActivator';
import { connectProxyResolver } from 'vs/workbench/services/extensions/node/proxyResolver';
import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadService';
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
class NodeModuleRequireInterceptor extends RequireInterceptor {
protected _installInterceptor(): void {
const that = this;
const node_module = <any>require.__$__nodeRequire('module');
const original = node_module._load;
node_module._load = function load(request: string, parent: { filename: string; }, isMain: any) {
for (let alternativeModuleName of that._alternatives) {
let alternative = alternativeModuleName(request);
if (alternative) {
request = alternative;
break;
}
}
if (!that._factories.has(request)) {
return original.apply(this, arguments);
}
return that._factories.get(request)!.load(
request,
URI.file(parent.filename),
request => original.apply(this, [request, parent, isMain])
);
};
}
}
export class ExtHostExtensionService extends AbstractExtHostExtensionService {
readonly extensionRuntime = ExtensionRuntime.Node;
protected async _beforeAlmostReadyToRunExtensions(): Promise<void> {
// initialize API and register actors
const extensionApiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors);
// Register Download command
this._instaService.createInstance(ExtHostDownloadService);
// Register CLI Server for ipc
if (this._initData.remote.isRemote && this._initData.remote.authority) {
const cliServer = this._instaService.createInstance(CLIServer);
process.env['VSCODE_IPC_HOOK_CLI'] = cliServer.ipcHandlePath;
}
// Module loading tricks
const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry);
await interceptor.install();
// Do this when extension service exists, but extensions are not being activated yet.
const configProvider = await this._extHostConfiguration.getConfigProvider();
await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData);
// Use IPC messages to forward console-calls, note that the console is
// already patched to use`process.send()`
const nativeProcessSend = process.send!;
const mainThreadConsole = this._extHostContext.getProxy(MainContext.MainThreadConsole);
process.send = (...args) => {
if ((args as unknown[]).length === 0 || !args[0] || args[0].type !== '__$console') {
return nativeProcessSend.apply(process, args);
}
mainThreadConsole.$logExtensionHostMessage(args[0]);
return false;
};
}
protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined {
return extensionDescription.main;
}
protected _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
if (module.scheme !== Schemas.file) {
throw new Error(`Cannot load URI: '${module}', must be of file-scheme`);
}
let r: T | null = null;
activationTimesBuilder.codeLoadingStart();
this._logService.info(`ExtensionService#loadCommonJSModule ${module.toString(true)}`);
this._logService.flush();
try {
r = require.__$__nodeRequire<T>(module.fsPath);
} catch (e) {
return Promise.reject(e);
} finally {
activationTimesBuilder.codeLoadingStop();
}
return Promise.resolve(r);
}
public async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {
if (!this._initData.remote.isRemote) {
return;
}
for (const key in env) {
const value = env[key];
if (value === null) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILogService, DelegatedLogService, LogLevel } from 'vs/platform/log/common/log';
import { ExtHostLogServiceShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { Schemas } from 'vs/base/common/network';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import { dirname } from 'vs/base/common/resources';
export class ExtHostLogService extends DelegatedLogService implements ILogService, ExtHostLogServiceShape {
constructor(
@IExtHostInitDataService initData: IExtHostInitDataService,
) {
if (initData.logFile.scheme !== Schemas.file) { throw new Error('Only file-logging supported'); }
super(new SpdLogService(ExtensionHostLogFileName, dirname(initData.logFile).fsPath, initData.logLevel));
}
$setLevel(level: LogLevel): void {
this.setLevel(level);
}
}

View File

@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MainThreadOutputServiceShape } from '../common/extHost.protocol';
import type * as vscode from 'vscode';
import { URI } from 'vs/base/common/uri';
import { join } from 'vs/base/common/path';
import { OutputAppender } from 'vs/workbench/services/output/node/outputAppender';
import { toLocalISOString } from 'vs/base/common/date';
import { dirExists, mkdirp } from 'vs/base/node/pfs';
import { AbstractExtHostOutputChannel, ExtHostPushOutputChannel, ExtHostOutputService, LazyOutputChannel } from 'vs/workbench/api/common/extHostOutput';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { MutableDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
export class ExtHostOutputChannelBackedByFile extends AbstractExtHostOutputChannel {
private _appender: OutputAppender;
constructor(name: string, appender: OutputAppender, proxy: MainThreadOutputServiceShape) {
super(name, false, URI.file(appender.file), proxy);
this._appender = appender;
}
append(value: string): void {
super.append(value);
this._appender.append(value);
this._onDidAppend.fire();
}
update(): void {
this._appender.flush();
super.update();
}
show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void {
this._appender.flush();
super.show(columnOrPreserveFocus, preserveFocus);
}
clear(): void {
this._appender.flush();
super.clear();
}
}
export class ExtHostOutputService2 extends ExtHostOutputService {
private _logsLocation: URI;
private _namePool: number = 1;
private readonly _channels: Map<string, AbstractExtHostOutputChannel> = new Map<string, AbstractExtHostOutputChannel>();
private readonly _visibleChannelDisposable = new MutableDisposable();
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@ILogService private readonly logService: ILogService,
@IExtHostInitDataService initData: IExtHostInitDataService,
) {
super(extHostRpc);
this._logsLocation = initData.logsLocation;
}
$setVisibleChannel(channelId: string): void {
if (channelId) {
const channel = this._channels.get(channelId);
if (channel) {
this._visibleChannelDisposable.value = channel.onDidAppend(() => channel.update());
}
}
}
createOutputChannel(name: string): vscode.OutputChannel {
name = name.trim();
if (!name) {
throw new Error('illegal argument `name`. must not be falsy');
}
const extHostOutputChannel = this._doCreateOutChannel(name);
extHostOutputChannel.then(channel => channel._id.then(id => this._channels.set(id, channel)));
return new LazyOutputChannel(name, extHostOutputChannel);
}
private async _doCreateOutChannel(name: string): Promise<AbstractExtHostOutputChannel> {
try {
const outputDirPath = join(this._logsLocation.fsPath, `output_logging_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`);
const exists = await dirExists(outputDirPath);
if (!exists) {
await mkdirp(outputDirPath);
}
const fileName = `${this._namePool++}-${name.replace(/[\\/:\*\?"<>\|]/g, '')}`;
const file = URI.file(join(outputDirPath, `${fileName}.log`));
const appender = new OutputAppender(fileName, file.fsPath);
return new ExtHostOutputChannelBackedByFile(name, appender, this._proxy);
} catch (error) {
// Do not crash if logger cannot be created
this.logService.error(error);
return new ExtHostPushOutputChannel(name, this._proxy);
}
}
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import * as pfs from 'vs/base/node/pfs';
import { ILogService } from 'vs/platform/log/common/log';
import { IFileQuery, IRawFileQuery, ISearchCompleteStats, isSerializedFileMatch, ISerializedSearchProgressItem, ITextQuery } from 'vs/workbench/services/search/common/search';
import { SearchService } from 'vs/workbench/services/search/node/rawSearchService';
import { RipgrepSearchProvider } from 'vs/workbench/services/search/node/ripgrepSearchProvider';
import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils';
import type * as vscode from 'vscode';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { ExtHostSearch, reviveQuery } from 'vs/workbench/api/common/extHostSearch';
import { Schemas } from 'vs/base/common/network';
import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager';
export class NativeExtHostSearch extends ExtHostSearch {
protected _pfs: typeof pfs = pfs; // allow extending for tests
private _internalFileSearchHandle: number = -1;
private _internalFileSearchProvider: SearchService | null = null;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService,
@IURITransformerService _uriTransformer: IURITransformerService,
@ILogService _logService: ILogService,
) {
super(extHostRpc, _uriTransformer, _logService);
if (initData.remote.isRemote && initData.remote.authority) {
this._registerEHSearchProviders();
}
}
private _registerEHSearchProviders(): void {
const outputChannel = new OutputChannel(this._logService);
this.registerTextSearchProvider(Schemas.file, new RipgrepSearchProvider(outputChannel));
this.registerInternalFileSearchProvider(Schemas.file, new SearchService());
}
private registerInternalFileSearchProvider(scheme: string, provider: SearchService): IDisposable {
const handle = this._handlePool++;
this._internalFileSearchProvider = provider;
this._internalFileSearchHandle = handle;
this._proxy.$registerFileSearchProvider(handle, this._transformScheme(scheme));
return toDisposable(() => {
this._internalFileSearchProvider = null;
this._proxy.$unregisterProvider(handle);
});
}
$provideFileSearchResults(handle: number, session: number, rawQuery: IRawFileQuery, token: vscode.CancellationToken): Promise<ISearchCompleteStats> {
const query = reviveQuery(rawQuery);
if (handle === this._internalFileSearchHandle) {
return this.doInternalFileSearch(handle, session, query, token);
}
return super.$provideFileSearchResults(handle, session, rawQuery, token);
}
private doInternalFileSearch(handle: number, session: number, rawQuery: IFileQuery, token: vscode.CancellationToken): Promise<ISearchCompleteStats> {
const onResult = (ev: ISerializedSearchProgressItem) => {
if (isSerializedFileMatch(ev)) {
ev = [ev];
}
if (Array.isArray(ev)) {
this._proxy.$handleFileMatch(handle, session, ev.map(m => URI.file(m.path)));
return;
}
if (ev.message) {
this._logService.debug('ExtHostSearch', ev.message);
}
};
if (!this._internalFileSearchProvider) {
throw new Error('No internal file search handler');
}
return <Promise<ISearchCompleteStats>>this._internalFileSearchProvider.doFileSearch(rawQuery, onResult, token);
}
$clearCache(cacheKey: string): Promise<void> {
if (this._internalFileSearchProvider) {
this._internalFileSearchProvider.clearCache(cacheKey);
}
return super.$clearCache(cacheKey);
}
protected createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider): TextSearchManager {
return new NativeTextSearchManager(query, provider);
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { URI, UriComponents } from 'vs/base/common/uri';
import { win32 } from 'vs/base/node/processes';
import * as types from 'vs/workbench/api/common/extHostTypes';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import type * as vscode from 'vscode';
import * as tasks from '../common/shared/tasks';
import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { ExtHostTaskBase, TaskHandleDTO, TaskDTO, CustomExecutionDTO, HandlerData } from 'vs/workbench/api/common/extHostTask';
import { Schemas } from 'vs/base/common/network';
import { ILogService } from 'vs/platform/log/common/log';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
export class ExtHostTask extends ExtHostTaskBase {
private _variableResolver: ExtHostVariableResolverService | undefined;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService,
@IExtHostWorkspace private readonly workspaceService: IExtHostWorkspace,
@IExtHostDocumentsAndEditors editorService: IExtHostDocumentsAndEditors,
@IExtHostConfiguration configurationService: IExtHostConfiguration,
@IExtHostTerminalService extHostTerminalService: IExtHostTerminalService,
@ILogService logService: ILogService,
@IExtHostApiDeprecationService deprecationService: IExtHostApiDeprecationService
) {
super(extHostRpc, initData, workspaceService, editorService, configurationService, extHostTerminalService, logService, deprecationService);
if (initData.remote.isRemote && initData.remote.authority) {
this.registerTaskSystem(Schemas.vscodeRemote, {
scheme: Schemas.vscodeRemote,
authority: initData.remote.authority,
platform: process.platform
});
}
this._proxy.$registerSupportedExecutions(true, true, true);
}
public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise<vscode.TaskExecution> {
const tTask = (task as types.Task);
// We have a preserved ID. So the task didn't change.
if (tTask._id !== undefined) {
// Always get the task execution first to prevent timing issues when retrieving it later
const handleDto = TaskHandleDTO.from(tTask, this.workspaceService);
const executionDTO = await this._proxy.$getTaskExecution(handleDto);
if (executionDTO.task === undefined) {
throw new Error('Task from execution DTO is undefined');
}
const execution = await this.getTaskExecution(executionDTO, task);
this._proxy.$executeTask(handleDto).catch(() => { /* The error here isn't actionable. */ });
return execution;
} else {
const dto = TaskDTO.from(task, extension);
if (dto === undefined) {
return Promise.reject(new Error('Task is not valid'));
}
// If this task is a custom execution, then we need to save it away
// in the provided custom execution map that is cleaned up after the
// task is executed.
if (CustomExecutionDTO.is(dto.execution)) {
await this.addCustomExecution(dto, task, false);
}
// Always get the task execution first to prevent timing issues when retrieving it later
const execution = await this.getTaskExecution(await this._proxy.$getTaskExecution(dto), task);
this._proxy.$executeTask(dto).catch(() => { /* The error here isn't actionable. */ });
return execution;
}
}
protected provideTasksInternal(validTypes: { [key: string]: boolean; }, taskIdPromises: Promise<void>[], handler: HandlerData, value: vscode.Task[] | null | undefined): { tasks: tasks.TaskDTO[], extension: IExtensionDescription } {
const taskDTOs: tasks.TaskDTO[] = [];
if (value) {
for (let task of value) {
this.checkDeprecation(task, handler);
if (!task.definition || !validTypes[task.definition.type]) {
this._logService.warn(`The task [${task.source}, ${task.name}] uses an undefined task type. The task will be ignored in the future.`);
}
const taskDTO: tasks.TaskDTO | undefined = TaskDTO.from(task, handler.extension);
if (taskDTO) {
taskDTOs.push(taskDTO);
if (CustomExecutionDTO.is(taskDTO.execution)) {
// The ID is calculated on the main thread task side, so, let's call into it here.
// We need the task id's pre-computed for custom task executions because when OnDidStartTask
// is invoked, we have to be able to map it back to our data.
taskIdPromises.push(this.addCustomExecution(taskDTO, task, true));
}
}
}
}
return {
tasks: taskDTOs,
extension: handler.extension
};
}
protected async resolveTaskInternal(resolvedTaskDTO: tasks.TaskDTO): Promise<tasks.TaskDTO | undefined> {
return resolvedTaskDTO;
}
private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise<ExtHostVariableResolverService> {
if (this._variableResolver === undefined) {
const configProvider = await this._configurationService.getConfigProvider();
this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, process.env as IProcessEnvironment);
}
return this._variableResolver;
}
public async $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }> {
const uri: URI = URI.revive(uriComponents);
const result = {
process: <unknown>undefined as string,
variables: Object.create(null)
};
const workspaceFolder = await this._workspaceProvider.resolveWorkspaceFolder(uri);
const workspaceFolders = await this._workspaceProvider.getWorkspaceFolders2();
if (!workspaceFolders || !workspaceFolder) {
throw new Error('Unexpected: Tasks can only be run in a workspace folder');
}
const resolver = await this.getVariableResolver(workspaceFolders);
const ws: IWorkspaceFolder = {
uri: workspaceFolder.uri,
name: workspaceFolder.name,
index: workspaceFolder.index,
toResource: () => {
throw new Error('Not implemented');
}
};
for (let variable of toResolve.variables) {
result.variables[variable] = resolver.resolve(ws, variable);
}
if (toResolve.process !== undefined) {
let paths: string[] | undefined = undefined;
if (toResolve.process.path !== undefined) {
paths = toResolve.process.path.split(path.delimiter);
for (let i = 0; i < paths.length; i++) {
paths[i] = resolver.resolve(ws, paths[i]);
}
}
result.process = await win32.findExecutable(
resolver.resolve(ws, toResolve.process.name),
toResolve.process.cwd !== undefined ? resolver.resolve(ws, toResolve.process.cwd) : undefined,
paths
);
}
return result;
}
public $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> {
return this._terminalService.$getDefaultShellAndArgs(true);
}
public async $jsonTasksSupported(): Promise<boolean> {
return true;
}
public async $findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string> {
return win32.findExecutable(command, cwd, paths);
}
}

View File

@@ -0,0 +1,232 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as vscode from 'vscode';
import * as os from 'os';
import { URI, UriComponents } from 'vs/base/common/uri';
import * as platform from 'vs/base/common/platform';
import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostConfiguration, ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration';
import { ILogService } from 'vs/platform/log/common/log';
import { IShellLaunchConfig, ITerminalEnvironment, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess';
import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService';
import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { getSystemShell, detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal';
import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment';
import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
export class ExtHostTerminalService extends BaseExtHostTerminalService {
private _variableResolver: ExtHostVariableResolverService | undefined;
private _lastActiveWorkspace: IWorkspaceFolder | undefined;
// TODO: Pull this from main side
private _isWorkspaceShellAllowed: boolean = false;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostConfiguration private _extHostConfiguration: ExtHostConfiguration,
@IExtHostWorkspace private _extHostWorkspace: ExtHostWorkspace,
@IExtHostDocumentsAndEditors private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors,
@ILogService private _logService: ILogService,
@IExtHostInitDataService private _extHostInitDataService: IExtHostInitDataService
) {
super(true, extHostRpc);
this._updateLastActiveWorkspace();
this._updateVariableResolver();
this._registerListeners();
}
public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, { name, shellPath, shellArgs }, name);
this._terminals.push(terminal);
terminal.create(shellPath, shellArgs);
return terminal;
}
public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, options, options.name);
this._terminals.push(terminal);
terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env, /*options.waitOnExit*/ undefined, options.strictEnv, options.hideFromUser, isFeatureTerminal);
return terminal;
}
public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string {
const fetchSetting = (key: string): { userValue: string | string[] | undefined, value: string | string[] | undefined, defaultValue: string | string[] | undefined } => {
const setting = configProvider
.getConfiguration(key.substr(0, key.lastIndexOf('.')))
.inspect<string | string[]>(key.substr(key.lastIndexOf('.') + 1));
return this._apiInspectConfigToPlain<string | string[]>(setting);
};
return terminalEnvironment.getDefaultShell(
fetchSetting,
this._isWorkspaceShellAllowed,
getSystemShell(platform.platform),
process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'),
process.env.windir,
terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver),
this._logService,
useAutomationShell
);
}
public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string {
const fetchSetting = (key: string): { userValue: string | string[] | undefined, value: string | string[] | undefined, defaultValue: string | string[] | undefined } => {
const setting = configProvider
.getConfiguration(key.substr(0, key.lastIndexOf('.')))
.inspect<string | string[]>(key.substr(key.lastIndexOf('.') + 1));
return this._apiInspectConfigToPlain<string | string[]>(setting);
};
return terminalEnvironment.getDefaultShellArgs(fetchSetting, this._isWorkspaceShellAllowed, useAutomationShell, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), this._logService);
}
private _apiInspectConfigToPlain<T>(
config: { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T, workspaceFolderValue?: T } | undefined
): { userValue: T | undefined, value: T | undefined, defaultValue: T | undefined } {
return {
userValue: config ? config.globalValue : undefined,
value: config ? config.workspaceValue : undefined,
defaultValue: config ? config.defaultValue : undefined,
};
}
private async _getNonInheritedEnv(): Promise<platform.IProcessEnvironment> {
const env = await getMainProcessParentEnv();
env.VSCODE_IPC_HOOK_CLI = process.env['VSCODE_IPC_HOOK_CLI']!;
return env;
}
private _registerListeners(): void {
this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(() => this._updateLastActiveWorkspace());
this._extHostWorkspace.onDidChangeWorkspace(() => this._updateVariableResolver());
}
private _updateLastActiveWorkspace(): void {
const activeEditor = this._extHostDocumentsAndEditors.activeEditor();
if (activeEditor) {
this._lastActiveWorkspace = this._extHostWorkspace.getWorkspaceFolder(activeEditor.document.uri) as IWorkspaceFolder;
}
}
private async _updateVariableResolver(): Promise<void> {
const configProvider = await this._extHostConfiguration.getConfigProvider();
const workspaceFolders = await this._extHostWorkspace.getWorkspaceFolders2();
this._variableResolver = new ExtHostVariableResolverService(workspaceFolders || [], this._extHostDocumentsAndEditors, configProvider, process.env as platform.IProcessEnvironment);
}
public async $spawnExtHostProcess(id: number, shellLaunchConfigDto: IShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined> {
const shellLaunchConfig: IShellLaunchConfig = {
name: shellLaunchConfigDto.name,
executable: shellLaunchConfigDto.executable,
args: shellLaunchConfigDto.args,
cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd),
env: shellLaunchConfigDto.env
};
// Merge in shell and args from settings
const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
const configProvider = await this._extHostConfiguration.getConfigProvider();
if (!shellLaunchConfig.executable) {
shellLaunchConfig.executable = this.getDefaultShell(false, configProvider);
shellLaunchConfig.args = this.getDefaultShellArgs(false, configProvider);
} else {
if (this._variableResolver) {
shellLaunchConfig.executable = this._variableResolver.resolve(this._lastActiveWorkspace, shellLaunchConfig.executable);
if (shellLaunchConfig.args) {
if (Array.isArray(shellLaunchConfig.args)) {
const resolvedArgs: string[] = [];
for (const arg of shellLaunchConfig.args) {
resolvedArgs.push(this._variableResolver.resolve(this._lastActiveWorkspace, arg));
}
shellLaunchConfig.args = resolvedArgs;
} else {
shellLaunchConfig.args = this._variableResolver.resolve(this._lastActiveWorkspace, shellLaunchConfig.args);
}
}
}
}
const activeWorkspaceRootUri = URI.revive(activeWorkspaceRootUriComponents);
let lastActiveWorkspace: IWorkspaceFolder | undefined;
if (activeWorkspaceRootUriComponents && activeWorkspaceRootUri) {
// Get the environment
const apiLastActiveWorkspace = await this._extHostWorkspace.getWorkspaceFolder(activeWorkspaceRootUri);
if (apiLastActiveWorkspace) {
lastActiveWorkspace = {
uri: apiLastActiveWorkspace.uri,
name: apiLastActiveWorkspace.name,
index: apiLastActiveWorkspace.index,
toResource: () => {
throw new Error('Not implemented');
}
};
}
}
// Get the initial cwd
const terminalConfig = configProvider.getConfiguration('terminal.integrated');
const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), terminalEnvironment.createVariableResolver(lastActiveWorkspace, this._variableResolver), activeWorkspaceRootUri, terminalConfig.cwd, this._logService);
shellLaunchConfig.cwd = initialCwd;
const envFromConfig = this._apiInspectConfigToPlain(configProvider.getConfiguration('terminal.integrated').inspect<ITerminalEnvironment>(`env.${platformKey}`));
const baseEnv = terminalConfig.get<boolean>('inheritEnv', true) ? process.env as platform.IProcessEnvironment : await this._getNonInheritedEnv();
const env = terminalEnvironment.createTerminalEnvironment(
shellLaunchConfig,
envFromConfig,
terminalEnvironment.createVariableResolver(lastActiveWorkspace, this._variableResolver),
isWorkspaceShellAllowed,
this._extHostInitDataService.version,
terminalConfig.get<'auto' | 'off' | 'on'>('detectLocale', 'auto'),
baseEnv
);
// Apply extension environment variable collections to the environment
if (!shellLaunchConfig.strictEnv) {
const mergedCollection = new MergedEnvironmentVariableCollection(this._environmentVariableCollections);
mergedCollection.applyToProcessEnvironment(env);
}
this._proxy.$sendResolvedLaunchConfig(id, shellLaunchConfig);
// Fork the process and listen for messages
this._logService.debug(`Terminal process launching on ext host`, { shellLaunchConfig, initialCwd, cols, rows, env });
// TODO: Support conpty on remote, it doesn't seem to work for some reason?
// TODO: When conpty is enabled, only enable it when accessibilityMode is off
const enableConpty = false; //terminalConfig.get('windowsEnableConpty') as boolean;
const terminalProcess = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, process.env as platform.IProcessEnvironment, enableConpty, this._logService);
this._setupExtHostProcessListeners(id, terminalProcess);
const error = await terminalProcess.start();
if (error) {
// TODO: Teardown?
return error;
}
return undefined;
}
public $getAvailableShells(): Promise<IShellDefinitionDto[]> {
return detectAvailableShells();
}
public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise<IShellAndArgsDto> {
const configProvider = await this._extHostConfiguration.getConfigProvider();
return {
shell: this.getDefaultShell(useAutomationShell, configProvider),
args: this.getDefaultShellArgs(useAutomationShell, configProvider)
};
}
public $acceptWorkspacePermissionsChanged(isAllowed: boolean): void {
this._isWorkspaceShellAllowed = isAllowed;
}
}

View File

@@ -0,0 +1,250 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MainThreadTunnelServiceShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import type * as vscode from 'vscode';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { URI } from 'vs/base/common/uri';
import { exec } from 'child_process';
import * as resources from 'vs/base/common/resources';
import * as fs from 'fs';
import { isLinux } from 'vs/base/common/platform';
import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { asPromise } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { TunnelOptions } from 'vs/platform/remote/common/tunnel';
class ExtensionTunnel implements vscode.Tunnel {
private _onDispose: Emitter<void> = new Emitter();
onDidDispose: Event<void> = this._onDispose.event;
constructor(
public readonly remoteAddress: { port: number, host: string },
public readonly localAddress: { port: number, host: string } | string,
private readonly _dispose: () => void) { }
dispose(): void {
this._onDispose.fire();
this._dispose();
}
}
export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService {
readonly _serviceBrand: undefined;
private readonly _proxy: MainThreadTunnelServiceShape;
private _forwardPortProvider: ((tunnelOptions: TunnelOptions) => Thenable<vscode.Tunnel> | undefined) | undefined;
private _showCandidatePort: (host: string, port: number, detail: string) => Thenable<boolean> = () => { return Promise.resolve(true); };
private _extensionTunnels: Map<string, Map<number, vscode.Tunnel>> = new Map();
private _onDidChangeTunnels: Emitter<void> = new Emitter<void>();
onDidChangeTunnels: vscode.Event<void> = this._onDidChangeTunnels.event;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService
) {
super();
this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService);
if (initData.remote.isRemote && initData.remote.authority) {
this.registerCandidateFinder();
}
}
async openTunnel(forward: TunnelOptions): Promise<vscode.Tunnel | undefined> {
const tunnel = await this._proxy.$openTunnel(forward);
if (tunnel) {
const disposableTunnel: vscode.Tunnel = new ExtensionTunnel(tunnel.remoteAddress, tunnel.localAddress, () => {
return this._proxy.$closeTunnel(tunnel.remoteAddress);
});
this._register(disposableTunnel);
return disposableTunnel;
}
return undefined;
}
async getTunnels(): Promise<vscode.TunnelDescription[]> {
return this._proxy.$getTunnels();
}
registerCandidateFinder(): Promise<void> {
return this._proxy.$registerCandidateFinder();
}
$filterCandidates(candidates: { host: string, port: number, detail: string }[]): Promise<boolean[]> {
return Promise.all(candidates.map(candidate => {
return this._showCandidatePort(candidate.host, candidate.port, candidate.detail);
}));
}
async setTunnelExtensionFunctions(provider: vscode.RemoteAuthorityResolver | undefined): Promise<IDisposable> {
if (provider) {
if (provider.showCandidatePort) {
this._showCandidatePort = provider.showCandidatePort;
await this._proxy.$setCandidateFilter();
}
if (provider.tunnelFactory) {
this._forwardPortProvider = provider.tunnelFactory;
await this._proxy.$setTunnelProvider();
}
} else {
this._forwardPortProvider = undefined;
}
await this._proxy.$tunnelServiceReady();
return toDisposable(() => {
this._forwardPortProvider = undefined;
});
}
async $closeTunnel(remote: { host: string, port: number }): Promise<void> {
if (this._extensionTunnels.has(remote.host)) {
const hostMap = this._extensionTunnels.get(remote.host)!;
if (hostMap.has(remote.port)) {
hostMap.get(remote.port)!.dispose();
hostMap.delete(remote.port);
}
}
}
async $onDidTunnelsChange(): Promise<void> {
this._onDidChangeTunnels.fire();
}
$forwardPort(tunnelOptions: TunnelOptions): Promise<TunnelDto> | undefined {
if (this._forwardPortProvider) {
const providedPort = this._forwardPortProvider!(tunnelOptions);
if (providedPort !== undefined) {
return asPromise(() => providedPort).then(tunnel => {
if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) {
this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map());
}
this._extensionTunnels.get(tunnelOptions.remoteAddress.host)!.set(tunnelOptions.remoteAddress.port, tunnel);
this._register(tunnel.onDidDispose(() => this._proxy.$closeTunnel(tunnel.remoteAddress)));
return Promise.resolve(TunnelDto.fromApiTunnel(tunnel));
});
}
}
return undefined;
}
async $findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> {
if (!isLinux) {
return [];
}
const ports: { host: string, port: number, detail: string }[] = [];
let tcp: string = '';
let tcp6: string = '';
try {
tcp = fs.readFileSync('/proc/net/tcp', 'utf8');
tcp6 = fs.readFileSync('/proc/net/tcp6', 'utf8');
} catch (e) {
// File reading error. No additional handling needed.
}
const procSockets: string = await (new Promise(resolve => {
exec('ls -l /proc/[0-9]*/fd/[0-9]* | grep socket:', (error, stdout, stderr) => {
resolve(stdout);
});
}));
const procChildren = fs.readdirSync('/proc');
const processes: { pid: number, cwd: string, cmd: string }[] = [];
for (let childName of procChildren) {
try {
const pid: number = Number(childName);
const childUri = resources.joinPath(URI.file('/proc'), childName);
const childStat = fs.statSync(childUri.fsPath);
if (childStat.isDirectory() && !isNaN(pid)) {
const cwd = fs.readlinkSync(resources.joinPath(childUri, 'cwd').fsPath);
const cmd = fs.readFileSync(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8');
processes.push({ pid, cwd, cmd });
}
} catch (e) {
//
}
}
const connections: { socket: number, ip: string, port: number }[] = this.loadListeningPorts(tcp, tcp6);
const sockets = this.getSockets(procSockets);
const socketMap = sockets.reduce((m, socket) => {
m[socket.socket] = socket;
return m;
}, {} as Record<string, typeof sockets[0]>);
const processMap = processes.reduce((m, process) => {
m[process.pid] = process;
return m;
}, {} as Record<string, typeof processes[0]>);
connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => {
const command = processMap[socketMap[socket].pid].cmd;
if (!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) && (command.indexOf('out/vs/server/main.js') === -1)) {
ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd });
}
});
return ports;
}
private getSockets(stdout: string) {
const lines = stdout.trim().split('\n');
return lines.map(line => {
const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!;
return {
pid: parseInt(match[1], 10),
socket: parseInt(match[2], 10)
};
});
}
private loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] {
const table = ([] as Record<string, string>[]).concat(...stdouts.map(this.loadConnectionTable));
return [
...new Map(
table.filter(row => row.st === '0A')
.map(row => {
const address = row.local_address.split(':');
return {
socket: parseInt(row.inode, 10),
ip: this.parseIpAddress(address[0]),
port: parseInt(address[1], 16)
};
}).map(port => [port.ip + ':' + port.port, port])
).values()
];
}
private parseIpAddress(hex: string): string {
let result = '';
if (hex.length === 8) {
for (let i = hex.length - 2; i >= 0; i -= 2) {
result += parseInt(hex.substr(i, 2), 16);
if (i !== 0) {
result += '.';
}
}
} else {
for (let i = hex.length - 4; i >= 0; i -= 4) {
result += parseInt(hex.substr(i, 4), 16).toString(16);
if (i !== 0) {
result += ':';
}
}
}
return result;
}
private loadConnectionTable(stdout: string): Record<string, string>[] {
const lines = stdout.trim().split('\n');
const names = lines.shift()!.trim().split(/\s+/)
.filter(name => name !== 'rx_queue' && name !== 'tm->when');
const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => {
obj[names[i] || i] = value;
return obj;
}, {} as Record<string, string>));
return table;
}
}