diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index bc11cdd6f12..213d0bdf512 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -86,8 +86,8 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise { diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index 333b3734596..b3857616f70 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -34,12 +34,18 @@ export interface RunCommandPipeArgs { args: any[]; } -export class CLIServer { +export interface ICommandsExecuter { + executeCommand(id: string, ...args: any[]): Promise; +} - private _server: http.Server; - private _ipcHandlePath: string | undefined; +export class CLIServerBase { + private readonly _server: http.Server; - constructor(@IExtHostCommands private _commands: IExtHostCommands, @ILogService private logService: ILogService) { + 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); @@ -52,8 +58,6 @@ export class CLIServer { } private async setup(): Promise { - this._ipcHandlePath = createRandomIPCHandle(); - try { this._server.listen(this.ipcHandlePath); this._server.on('error', err => this.logService.error(err)); @@ -176,3 +180,12 @@ export class CLIServer { } } } + +export class CLIServer extends CLIServerBase { + constructor( + @IExtHostCommands commands: IExtHostCommands, + @ILogService logService: ILogService + ) { + super(commands, logService, createRandomIPCHandle()); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts new file mode 100644 index 00000000000..4fd6bec36f1 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Barrier } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; +import { URI } from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteTerminalService, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalProcessExecCommandEvent, IShellLaunchConfigDto, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalConfigHelper, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; + +export class RemoteTerminalService extends Disposable implements IRemoteTerminalService { + public _serviceBrand: undefined; + + private readonly _remoteTerminalChannel: RemoteTerminalChannelClient | null; + + constructor( + @ITerminalService _terminalService: ITerminalService, + @ITerminalInstanceService readonly terminalInstanceService: ITerminalInstanceService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @ILogService private readonly _logService: ILogService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ICommandService private readonly _commandService: ICommandService, + ) { + super(); + const connection = this._remoteAgentService.getConnection(); + if (connection) { + this._remoteTerminalChannel = this._instantiationService.createInstance(RemoteTerminalChannelClient, connection.remoteAuthority, connection.getChannel(REMOTE_TERMINAL_CHANNEL_NAME)); + } else { + this._remoteTerminalChannel = null; + } + } + + public async createRemoteTerminalProcess(terminalId: number, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, configHelper: ITerminalConfigHelper,): Promise { + if (!this._remoteTerminalChannel) { + throw new Error(`Cannot create remote terminal when there is no remote!`); + } + + return new RemoteTerminalProcess(terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, configHelper, this._remoteTerminalChannel, this._remoteAgentService, this._logService, this._commandService); + } +} + +export class RemoteTerminalProcess extends Disposable implements ITerminalChildProcess { + + public readonly _onProcessData = this._register(new Emitter()); + public readonly onProcessData: Event = this._onProcessData.event; + private readonly _onProcessExit = this._register(new Emitter()); + public readonly onProcessExit: Event = this._onProcessExit.event; + public readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); + public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } + private readonly _onProcessTitleChanged = this._register(new Emitter()); + public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; + private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); + public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } + + private _startBarrier: Barrier; + private _remoteTerminalId: number; + + constructor( + private readonly _terminalId: number, + private readonly _shellLaunchConfig: IShellLaunchConfig, + private readonly _activeWorkspaceRootUri: URI | undefined, + private readonly _cols: number, + private readonly _rows: number, + private readonly _configHelper: ITerminalConfigHelper, + private readonly _remoteTerminalChannel: RemoteTerminalChannelClient, + private readonly _remoteAgentService: IRemoteAgentService, + private readonly _logService: ILogService, + private readonly _commandService: ICommandService, + ) { + super(); + this._startBarrier = new Barrier(); + this._remoteTerminalId = 0; + } + + public async start(): Promise { + + // TODO@remoteAgentTerminals: Add a loading title only if this terminal is + // instantiated before a connection is up and running + setTimeout(() => this._onProcessTitleChanged.fire(nls.localize('terminal.integrated.starting', "Starting...")), 0); + + // Fetch the environment to check shell permissions + const env = await this._remoteAgentService.getEnvironment(); + if (!env) { + // Extension host processes are only allowed in remote extension hosts currently + throw new Error('Could not fetch remote environment'); + } + + const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(env.os); + + const shellLaunchConfigDto: IShellLaunchConfigDto = { + name: this._shellLaunchConfig.name, + executable: this._shellLaunchConfig.executable, + args: this._shellLaunchConfig.args, + cwd: this._shellLaunchConfig.cwd, + env: this._shellLaunchConfig.env + }; + + this._logService.trace('Spawning remote agent process', { terminalId: this._terminalId, shellLaunchConfigDto }); + + const result = await this._remoteTerminalChannel.createTerminalProcess( + shellLaunchConfigDto, + this._activeWorkspaceRootUri, + this._cols, + this._rows, + isWorkspaceShellAllowed, + ); + + this._remoteTerminalId = result.terminalId; + this._register(this._remoteTerminalChannel.onTerminalProcessEvent(this._remoteTerminalId)(event => { + switch (event.type) { + case 'ready': + return this._onProcessReady.fire({ pid: event.pid, cwd: event.cwd }); + case 'titleChanged': + return this._onProcessTitleChanged.fire(event.title); + case 'data': + return this._onProcessData.fire(event.data); + case 'exit': + return this._onProcessExit.fire(event.exitCode); + case 'execCommand': + return this._execCommand(event); + } + })); + + this._onProcessResolvedShellLaunchConfig.fire(reviveIShellLaunchConfig(result.resolvedShellLaunchConfig)); + + const startResult = await this._remoteTerminalChannel.startTerminalProcess(this._remoteTerminalId); + + if (typeof startResult !== 'undefined') { + // An error occurred + return startResult; + } + + this._startBarrier.open(); + return undefined; + } + + public shutdown(immediate: boolean): void { + this._startBarrier.wait().then(_ => { + this._remoteTerminalChannel.shutdownTerminalProcess(this._remoteTerminalId, immediate); + }); + } + + public input(data: string): void { + this._startBarrier.wait().then(_ => { + this._remoteTerminalChannel.sendInputToTerminalProcess(this._remoteTerminalId, data); + }); + } + + public resize(cols: number, rows: number): void { + this._startBarrier.wait().then(_ => { + this._remoteTerminalChannel.resizeTerminalProcess(this._remoteTerminalId, cols, rows); + }); + } + + public async getInitialCwd(): Promise { + await this._startBarrier.wait(); + return this._remoteTerminalChannel.getTerminalInitialCwd(this._remoteTerminalId); + } + + public async getCwd(): Promise { + await this._startBarrier.wait(); + return this._remoteTerminalChannel.getTerminalCwd(this._remoteTerminalId); + } + + /** + * TODO@roblourens I don't think this does anything useful in the EH and the value isn't used + */ + public async getLatency(): Promise { + return 0; + } + + private async _execCommand(event: IRemoteTerminalProcessExecCommandEvent): Promise { + const reqId = event.reqId; + const commandArgs = event.commandArgs.map(arg => revive(arg)); + try { + const result = await this._commandService.executeCommand(event.commandId, ...commandArgs); + this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._remoteTerminalId, reqId, false, result); + } catch (err) { + this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._remoteTerminalId, reqId, true, err); + } + } +} + +function reviveIShellLaunchConfig(dto: IShellLaunchConfigDto): IShellLaunchConfig { + return { + name: dto.name, + executable: dto.executable, + args: dto.args, + cwd: ( + (typeof dto.cwd === 'string' || typeof dto.cwd === 'undefined') + ? dto.cwd + : URI.revive(dto.cwd) + ), + env: dto.env, + hideFromUser: dto.hideFromUser + }; +} + +registerSingleton(IRemoteTerminalService, RemoteTerminalService); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 84631cdac69..d7a4b61e0f9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -17,6 +17,7 @@ import { URI } from 'vs/base/common/uri'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalInstanceService = createDecorator('terminalInstanceService'); +export const IRemoteTerminalService = createDecorator('remoteTerminalService'); /** * A service used by TerminalInstance (and components owned by it) that allows it to break its @@ -174,6 +175,18 @@ export interface ITerminalService { requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise; } +export interface IRemoteExtHostEnvProvider { + getEnvironment(): Promise; +} + +export interface IRemoteTerminalService { + readonly _serviceBrand: undefined; + + dispose(): void; + + createRemoteTerminalProcess(terminalId: number, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, configHelper: ITerminalConfigHelper,): Promise; +} + /** * Similar to xterm.js' ILinkProvider but using promises and hides xterm.js internals (like buffer * positions, decorations, etc.) from the rest of vscode. This is the interface to use for diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index dd2cbff7668..0dd449b36f4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -18,7 +18,7 @@ import { Schemas } from 'vs/base/common/network'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; -import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IRemoteTerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -99,6 +99,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IPathService private readonly _pathService: IPathService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, + @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, ) { super(); this.ptyProcessReady = new Promise(c => { @@ -157,7 +158,12 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); - this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, this._configHelper); + const enableRemoteAgentTerminals = this._workspaceConfigurationService.getValue('terminal.integrated.serverSpawn'); + if (enableRemoteAgentTerminals) { + this._process = await this._remoteTerminalService.createRemoteTerminalProcess(this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, this._configHelper); + } else { + this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, this._configHelper); + } } else { this._process = await this._launchProcess(shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled); } diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts new file mode 100644 index 00000000000..c84ca3c0819 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -0,0 +1,309 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { ITerminalConfiguration, ITerminalEnvironment, ITerminalLaunchError, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { Schemas } from 'vs/base/common/network'; + +export const REMOTE_TERMINAL_CHANNEL_NAME = 'remoteterminal'; + +export interface IShellLaunchConfigDto { + name?: string; + executable?: string; + args?: string[] | string; + cwd?: string | UriComponents; + env?: { [key: string]: string | null; }; + hideFromUser?: boolean; +} + +export interface ISingleTerminalConfiguration { + userValue: T | undefined; + value: T | undefined; + defaultValue: T | undefined; +} + +export interface ICompleteTerminalConfiguration { + 'terminal.integrated.automationShell.windows': ISingleTerminalConfiguration; + 'terminal.integrated.automationShell.osx': ISingleTerminalConfiguration; + 'terminal.integrated.automationShell.linux': ISingleTerminalConfiguration; + 'terminal.integrated.shell.windows': ISingleTerminalConfiguration; + 'terminal.integrated.shell.osx': ISingleTerminalConfiguration; + 'terminal.integrated.shell.linux': ISingleTerminalConfiguration; + 'terminal.integrated.shellArgs.windows': ISingleTerminalConfiguration; + 'terminal.integrated.shellArgs.osx': ISingleTerminalConfiguration; + 'terminal.integrated.shellArgs.linux': ISingleTerminalConfiguration; + 'terminal.integrated.env.windows': ISingleTerminalConfiguration; + 'terminal.integrated.env.osx': ISingleTerminalConfiguration; + 'terminal.integrated.env.linux': ISingleTerminalConfiguration; + 'terminal.integrated.inheritEnv': boolean; + 'terminal.integrated.cwd': string; + 'terminal.integrated.detectLocale': 'auto' | 'off' | 'on'; +} + +export type ITerminalEnvironmentVariableCollections = [string, ISerializableEnvironmentVariableCollection][]; + +export interface IWorkspaceFolderData { + uri: UriComponents; + name: string; + index: number; +} + +export interface ICreateTerminalProcessArguments { + configuration: ICompleteTerminalConfiguration; + resolvedVariables: { [name: string]: string; }; + envVariableCollections: ITerminalEnvironmentVariableCollections; + shellLaunchConfig: IShellLaunchConfigDto; + workspaceFolders: IWorkspaceFolderData[]; + activeWorkspaceFolder: IWorkspaceFolderData | null; + activeFileResource: UriComponents | undefined; + cols: number; + rows: number; + isWorkspaceShellAllowed: boolean; + resolverEnv: { [key: string]: string | null; } | undefined +} + +export interface ICreateTerminalProcessResult { + terminalId: number; + resolvedShellLaunchConfig: IShellLaunchConfigDto; +} + +export interface IStartTerminalProcessArguments { + id: number; +} + +export interface ISendInputToTerminalProcessArguments { + id: number; + data: string; +} + +export interface IShutdownTerminalProcessArguments { + id: number; + immediate: boolean; +} + +export interface IResizeTerminalProcessArguments { + id: number; + cols: number; + rows: number; +} + +export interface IGetTerminalInitialCwdArguments { + id: number; +} + +export interface IGetTerminalCwdArguments { + id: number; +} + +export interface ISendCommandResultToTerminalProcessArguments { + id: number; + reqId: number; + isError: boolean; + payload: any; +} + +export interface IRemoteTerminalProcessReadyEvent { + type: 'ready'; + pid: number; + cwd: string; +} +export interface IRemoteTerminalProcessTitleChangedEvent { + type: 'titleChanged'; + title: string; +} +export interface IRemoteTerminalProcessDataEvent { + type: 'data' + data: string; +} +export interface IRemoteTerminalProcessExitEvent { + type: 'exit' + exitCode: number | undefined; +} +export interface IRemoteTerminalProcessExecCommandEvent { + type: 'execCommand'; + reqId: number; + commandId: string; + commandArgs: any[]; +} +export type IRemoteTerminalProcessEvent = ( + IRemoteTerminalProcessReadyEvent + | IRemoteTerminalProcessTitleChangedEvent + | IRemoteTerminalProcessDataEvent + | IRemoteTerminalProcessExitEvent + | IRemoteTerminalProcessExecCommandEvent +); + +export interface IOnTerminalProcessEventArguments { + id: number; +} + +export class RemoteTerminalChannelClient { + + constructor( + private readonly _remoteAuthority: string, + private readonly _channel: IChannel, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IConfigurationResolverService private readonly _resolverService: IConfigurationResolverService, + @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, + @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @ILogService private readonly _logService: ILogService, + @IEditorService private readonly _editorService: IEditorService, + ) { + } + + private _readSingleTemrinalConfiguration(key: string): ISingleTerminalConfiguration { + const result = this._configurationService.inspect(key); + return { + userValue: result.userValue, + value: result.value, + defaultValue: result.defaultValue, + }; + } + + public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { + const terminalConfig = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); + const configuration: ICompleteTerminalConfiguration = { + 'terminal.integrated.automationShell.windows': this._readSingleTemrinalConfiguration('terminal.integrated.automationShell.windows'), + 'terminal.integrated.automationShell.osx': this._readSingleTemrinalConfiguration('terminal.integrated.automationShell.osx'), + 'terminal.integrated.automationShell.linux': this._readSingleTemrinalConfiguration('terminal.integrated.automationShell.linux'), + 'terminal.integrated.shell.windows': this._readSingleTemrinalConfiguration('terminal.integrated.shell.windows'), + 'terminal.integrated.shell.osx': this._readSingleTemrinalConfiguration('terminal.integrated.shell.osx'), + 'terminal.integrated.shell.linux': this._readSingleTemrinalConfiguration('terminal.integrated.shell.linux'), + 'terminal.integrated.shellArgs.windows': this._readSingleTemrinalConfiguration('terminal.integrated.shellArgs.windows'), + 'terminal.integrated.shellArgs.osx': this._readSingleTemrinalConfiguration('terminal.integrated.shellArgs.osx'), + 'terminal.integrated.shellArgs.linux': this._readSingleTemrinalConfiguration('terminal.integrated.shellArgs.linux'), + 'terminal.integrated.env.windows': this._readSingleTemrinalConfiguration('terminal.integrated.env.windows'), + 'terminal.integrated.env.osx': this._readSingleTemrinalConfiguration('terminal.integrated.env.osx'), + 'terminal.integrated.env.linux': this._readSingleTemrinalConfiguration('terminal.integrated.env.linux'), + 'terminal.integrated.inheritEnv': terminalConfig.inheritEnv, + 'terminal.integrated.cwd': terminalConfig.cwd, + 'terminal.integrated.detectLocale': terminalConfig.detectLocale, + }; + + // We will use the resolver service to resolve all the variables in the config / launch config + // But then we will keep only some variables, since the rest need to be resolved on the remote side + const resolvedVariables = Object.create(null); + const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; + let allResolvedVariables: Map | undefined = undefined; + try { + allResolvedVariables = await this._resolverService.resolveWithInteraction(lastActiveWorkspace, { + shellLaunchConfig, + configuration + }); + } catch (err) { + this._logService.error(err); + } + if (allResolvedVariables) { + for (const [name, value] of allResolvedVariables.entries()) { + if (/^config:/.test(name) || name === 'selectedText' || name === 'lineNumber') { + resolvedVariables[name] = value; + } + } + } + + const envVariableCollections: ITerminalEnvironmentVariableCollections = []; + for (const [k, v] of this._environmentVariableService.collections.entries()) { + envVariableCollections.push([k, serializeEnvironmentVariableCollection(v.map)]); + } + + const resolverResult = await this._remoteAuthorityResolverService.resolveAuthority(this._remoteAuthority); + const resolverEnv = resolverResult.options && resolverResult.options.extensionHostEnv; + + const workspaceFolders = this._workspaceContextService.getWorkspace().folders; + const activeWorkspaceFolder = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : null; + + const activeFileResource = EditorResourceAccessor.getOriginalUri(this._editorService.activeEditor, { + supportSideBySide: SideBySideEditor.PRIMARY, + filterByScheme: [Schemas.file, Schemas.userData, Schemas.vscodeRemote] + }); + + const args: ICreateTerminalProcessArguments = { + configuration, + resolvedVariables, + envVariableCollections, + shellLaunchConfig, + workspaceFolders, + activeWorkspaceFolder, + activeFileResource, + cols, + rows, + isWorkspaceShellAllowed, + resolverEnv + }; + return await this._channel.call('$createTerminalProcess', args); + } + + public async startTerminalProcess(terminalId: number): Promise { + const args: IStartTerminalProcessArguments = { + id: terminalId + }; + return this._channel.call('$startTerminalProcess', args); + } + + public onTerminalProcessEvent(terminalId: number): Event { + const args: IOnTerminalProcessEventArguments = { + id: terminalId + }; + return this._channel.listen('$onTerminalProcessEvent', args); + } + + public sendInputToTerminalProcess(id: number, data: string): Promise { + const args: ISendInputToTerminalProcessArguments = { + id, data + }; + return this._channel.call('$sendInputToTerminalProcess', args); + } + + public shutdownTerminalProcess(id: number, immediate: boolean): Promise { + const args: IShutdownTerminalProcessArguments = { + id, immediate + }; + return this._channel.call('$shutdownTerminalProcess', args); + } + + public resizeTerminalProcess(id: number, cols: number, rows: number): Promise { + const args: IResizeTerminalProcessArguments = { + id, cols, rows + }; + return this._channel.call('$resizeTerminalProcess', args); + } + + public getTerminalInitialCwd(id: number): Promise { + const args: IGetTerminalInitialCwdArguments = { + id + }; + return this._channel.call('$getTerminalInitialCwd', args); + } + + public getTerminalCwd(id: number): Promise { + const args: IGetTerminalCwdArguments = { + id + }; + return this._channel.call('$getTerminalCwd', args); + } + + public sendCommandResultToTerminalProcess(id: number, reqId: number, isError: boolean, payload: any): Promise { + const args: ISendCommandResultToTerminalProcessArguments = { + id, + reqId, + isError, + payload + }; + return this._channel.call('$sendCommandResultToTerminalProcess', args); + } +} diff --git a/src/vs/workbench/contrib/terminal/node/terminal.ts b/src/vs/workbench/contrib/terminal/node/terminal.ts index c672f252827..8dc3b6c1533 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -16,27 +16,27 @@ import { normalize, basename } from 'vs/base/common/path'; * shell that the terminal uses by default. * @param p The platform to detect the shell of. */ -export function getSystemShell(p: platform.Platform): string { +export function getSystemShell(p: platform.Platform, environment: platform.IProcessEnvironment = process.env as platform.IProcessEnvironment): string { if (p === platform.Platform.Windows) { if (platform.isWindows) { - return getSystemShellWindows(); + return getSystemShellWindows(environment); } // Don't detect Windows shell when not on Windows - return processes.getWindowsShell(); + return processes.getWindowsShell(environment); } // Only use $SHELL for the current OS if (platform.isLinux && p === platform.Platform.Mac || platform.isMacintosh && p === platform.Platform.Linux) { return '/bin/bash'; } - return getSystemShellUnixLike(); + return getSystemShellUnixLike(environment); } let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null; -function getSystemShellUnixLike(): string { +function getSystemShellUnixLike(environment: platform.IProcessEnvironment): string { if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) { let unixLikeTerminal = 'sh'; - if (!platform.isWindows && process.env.SHELL) { - unixLikeTerminal = process.env.SHELL; + if (!platform.isWindows && environment.SHELL) { + unixLikeTerminal = environment.SHELL; // Some systems have $SHELL set to /bin/false which breaks the terminal if (unixLikeTerminal === '/bin/false') { unixLikeTerminal = '/bin/bash'; @@ -51,12 +51,12 @@ function getSystemShellUnixLike(): string { } let _TERMINAL_DEFAULT_SHELL_WINDOWS: string | null = null; -function getSystemShellWindows(): string { +function getSystemShellWindows(environment: platform.IProcessEnvironment): string { if (!_TERMINAL_DEFAULT_SHELL_WINDOWS) { const isAtLeastWindows10 = platform.isWindows && parseFloat(os.release()) >= 10; - const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); - const powerShellPath = `${process.env.windir}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`; - _TERMINAL_DEFAULT_SHELL_WINDOWS = isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell(); + const is32ProcessOn64Windows = environment.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const powerShellPath = `${environment.windir}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`; + _TERMINAL_DEFAULT_SHELL_WINDOWS = isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell(environment); } return _TERMINAL_DEFAULT_SHELL_WINDOWS; } diff --git a/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts index 33aca264cf7..5e7f3777d40 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalEnvironment.ts @@ -10,7 +10,7 @@ import { isString } from 'vs/base/common/types'; let mainProcessParentEnv: IProcessEnvironment | undefined; -export async function getMainProcessParentEnv(): Promise { +export async function getMainProcessParentEnv(baseEnvironment: IProcessEnvironment = process.env as IProcessEnvironment): Promise { if (mainProcessParentEnv) { return mainProcessParentEnv; } @@ -65,15 +65,15 @@ export async function getMainProcessParentEnv(): Promise { 'TMPDIR' ]; rootEnvVars.forEach(k => { - if (process.env[k]) { - mainProcessParentEnv![k] = process.env[k]!; + if (baseEnvironment[k]) { + mainProcessParentEnv![k] = baseEnvironment[k]!; } }); } // TODO: Windows should return a fresh environment block, might need native code? if (isWindows) { - mainProcessParentEnv = process.env as IProcessEnvironment; + mainProcessParentEnv = baseEnvironment; } return mainProcessParentEnv!; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 627f4de0024..6e64d7c797d 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -80,6 +80,7 @@ import 'vs/workbench/services/extensionRecommendations/common/workspaceExtension import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; import 'vs/workbench/services/remote/common/remoteExplorerService'; +import 'vs/workbench/contrib/terminal/browser/remoteTerminalService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; import 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';