diff --git a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index 8e974b94935ee841b28014cbcb6161f4ed535240..434652bfaf6c983159be4c45e0d3ec530ae3d629 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -5,31 +5,32 @@ 'use strict'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, ShellLaunchConfigDto } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { private _proxy: ExtHostTerminalServiceShape; - private _toDispose: IDisposable[]; + private _toDispose: IDisposable[] = []; + private _terminalProcesses: { [id: number]: ITerminalProcessExtHostProxy } = {}; constructor( extHostContext: IExtHostContext, @ITerminalService private terminalService: ITerminalService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); - this._toDispose = []; this._toDispose.push(terminalService.onInstanceCreated((terminalInstance) => { // Delay this message so the TerminalInstance constructor has a chance to finish and // return the ID normally to the extension host. The ID that is passed here will be used // to register non-extension API terminals in the extension host. setTimeout(() => this._onTerminalOpened(terminalInstance), 100); })); - this._toDispose.push(terminalService.onInstanceDisposed((terminalInstance) => this._onTerminalDisposed(terminalInstance))); - this._toDispose.push(terminalService.onInstanceProcessIdReady((terminalInstance) => this._onTerminalProcessIdReady(terminalInstance))); + this._toDispose.push(terminalService.onInstanceDisposed(terminalInstance => this._onTerminalDisposed(terminalInstance))); + this._toDispose.push(terminalService.onInstanceProcessIdReady(terminalInstance => this._onTerminalProcessIdReady(terminalInstance))); + this._toDispose.push(terminalService.onInstanceRequestExtHostProcess(request => this._onTerminalRequestExtHostProcess(request))); // Set initial ext host state this.terminalService.terminalInstances.forEach(t => { @@ -97,4 +98,36 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void { this._proxy.$acceptTerminalProcessId(terminalInstance.id, terminalInstance.processId); } + + private _onTerminalRequestExtHostProcess(request: ITerminalProcessExtHostRequest): void { + this._terminalProcesses[request.proxy.terminalId] = request.proxy; + const shellLaunchConfigDto: ShellLaunchConfigDto = { + name: request.shellLaunchConfig.name, + executable: request.shellLaunchConfig.executable, + args: request.shellLaunchConfig.args, + cwd: request.shellLaunchConfig.cwd, + env: request.shellLaunchConfig.env + }; + this._proxy.$createProcess(request.proxy.terminalId, shellLaunchConfigDto, request.cols, request.rows); + request.proxy.onInput(data => this._proxy.$acceptProcessInput(request.proxy.terminalId, data)); + request.proxy.onResize((cols, rows) => this._proxy.$acceptProcessResize(request.proxy.terminalId, cols, rows)); + request.proxy.onShutdown(() => this._proxy.$acceptProcessShutdown(request.proxy.terminalId)); + } + + public $sendProcessTitle(terminalId: number, title: string): void { + this._terminalProcesses[terminalId].emitTitle(title); + } + + public $sendProcessData(terminalId: number, data: string): void { + this._terminalProcesses[terminalId].emitData(data); + } + + public $sendProcessPid(terminalId: number, pid: number): void { + this._terminalProcesses[terminalId].emitPid(pid); + } + + public $sendProcessExit(terminalId: number, exitCode: number): void { + this._terminalProcesses[terminalId].emitExit(exitCode); + delete this._terminalProcesses[terminalId]; + } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index c91f1a47d3fa01b594a9708237e598436c3d2654..33fb8a807b9b768643c65ea1ea165020f4399689 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -114,7 +114,7 @@ export function createApiFactory( const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures)); const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService()); const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands)); - const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol)); + const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol, extHostConfiguration, extHostLogService)); const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostLogService)); const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, new ExtHostSearch(rpcProtocol)); const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace)); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 7e1410d4de18e8712d8300dd6abb14c88b1d868a..a6130803b52385f8f2dbf142a32d7f0ba2e8cda0 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -319,6 +319,11 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $hide(terminalId: number): void; $sendText(terminalId: number, text: string, addNewLine: boolean): void; $show(terminalId: number, preserveFocus: boolean): void; + + $sendProcessTitle(terminalId: number, title: string): void; + $sendProcessData(terminalId: number, data: string): void; + $sendProcessPid(terminalId: number, pid: number): void; + $sendProcessExit(terminalId: number, exitCode: number): void; } export interface MyQuickPickItems extends IPickOpenEntry { @@ -729,10 +734,22 @@ export interface ExtHostQuickOpenShape { $validateInput(input: string): TPromise; } +export interface ShellLaunchConfigDto { + name?: string; + executable?: string; + args?: string[] | string; + cwd?: string; + env?: { [key: string]: string }; +} + export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number): void; $acceptTerminalOpened(id: number, name: string): void; $acceptTerminalProcessId(id: number, processId: number): void; + $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void; + $acceptProcessInput(id: number, data: string): void; + $acceptProcessResize(id: number, cols: number, rows: number): void; + $acceptProcessShutdown(id: number): void; } export interface ExtHostSCMShape { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 4e993934fd2957456f01259c4ba4d82883c52d7d..cb546c2e8d8365c53d909d3980785eb70dd63933 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -5,11 +5,18 @@ 'use strict'; import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import * as os from 'os'; +import * as platform from 'vs/base/common/platform'; +import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; +import Uri from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext } from './extHost.protocol'; +import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext, ShellLaunchConfigDto } from 'vs/workbench/api/node/extHost.protocol'; +import { IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostTerminal implements vscode.Terminal { - private _name: string; private _id: number; private _proxy: MainThreadTerminalServiceShape; @@ -81,8 +88,11 @@ export class ExtHostTerminal implements vscode.Terminal { } public _setProcessId(processId: number): void { - this._pidPromiseComplete(processId); - this._pidPromiseComplete = null; + // The event may fire 2 times when the panel is restored + if (this._pidPromiseComplete) { + this._pidPromiseComplete(processId); + this._pidPromiseComplete = null; + } } private _queueApiRequest(callback: (...args: any[]) => void, args: any[]) { @@ -102,19 +112,22 @@ export class ExtHostTerminal implements vscode.Terminal { } export class ExtHostTerminalService implements ExtHostTerminalServiceShape { - private readonly _onDidCloseTerminal: Emitter; private readonly _onDidOpenTerminal: Emitter; private _proxy: MainThreadTerminalServiceShape; - private _terminals: ExtHostTerminal[]; + private _terminals: ExtHostTerminal[] = []; + private _terminalProcesses: { [id: number]: cp.ChildProcess } = {}; public get terminals(): ExtHostTerminal[] { return this._terminals; } - constructor(mainContext: IMainContext) { + constructor( + mainContext: IMainContext, + private _extHostConfiguration: ExtHostConfiguration, + private _logService: ILogService + ) { this._onDidCloseTerminal = new Emitter(); this._onDidOpenTerminal = new Emitter(); this._proxy = mainContext.getProxy(MainContext.MainThreadTerminalService); - this._terminals = []; } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { @@ -168,6 +181,99 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } } + public $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void { + // TODO: This function duplicates a lot of TerminalProcessManager.createProcess, ideally + // they would be merged into a single implementation. + + const terminalConfig = this._extHostConfiguration.getConfiguration('terminal.integrated'); + + const locale = terminalConfig.get('setLocaleVariables') ? platform.locale : undefined; + if (!shellLaunchConfig.executable) { + // TODO: This duplicates some of TerminalConfigHelper.mergeDefaultShellPathAndArgs and should be merged + // this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); + + const platformKey = platform.isWindows ? 'windows' : platform.isMacintosh ? 'osx' : 'linux'; + const shellConfigValue: string = terminalConfig.get(`shell.${platformKey}`); + const shellArgsConfigValue: string = terminalConfig.get(`shellArgs.${platformKey}`); + + shellLaunchConfig.executable = shellConfigValue; + shellLaunchConfig.args = shellArgsConfigValue; + } + + // TODO: Base the cwd on the last active workspace root + // const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); + // this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); + const initialCwd = os.homedir(); + + // TODO: Pull in and resolve config settings + // // Resolve env vars from config and shell + // const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); + // const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + // const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); + // const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); + // shellLaunchConfig.env = envFromShell; + + // Merge process env with the env from config + const parentEnv = { ...process.env }; + // terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + + // Continue env initialization, merging in the env from the launch + // config and adding keys that are needed to create the process + const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, initialCwd, locale, cols, rows); + let cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath; + const options = { env, cwd, execArgv: [] }; + + // Fork the process and listen for messages + this._logService.debug(`Terminal process launching on ext host`, options); + this._terminalProcesses[id] = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + this._terminalProcesses[id].on('message', (message: IMessageFromTerminalProcess) => { + switch (message.type) { + case 'pid': this._proxy.$sendProcessPid(id, message.content); break; + case 'title': this._proxy.$sendProcessTitle(id, message.content); break; + case 'data': this._proxy.$sendProcessData(id, message.content); break; + } + }); + this._terminalProcesses[id].on('exit', (exitCode) => this._onProcessExit(id, exitCode)); + } + + public $acceptProcessInput(id: number, data: string): void { + if (this._terminalProcesses[id].connected) { + this._terminalProcesses[id].send({ event: 'input', data }); + } + } + + public $acceptProcessResize(id: number, cols: number, rows: number): void { + if (this._terminalProcesses[id].connected) { + try { + this._terminalProcesses[id].send({ event: 'resize', cols, rows }); + } catch (error) { + // We tried to write to a closed pipe / channel. + if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { + throw (error); + } + } + } + } + + public $acceptProcessShutdown(id: number): void { + if (this._terminalProcesses[id].connected) { + this._terminalProcesses[id].send({ event: 'shutdown' }); + } + } + + private _onProcessExit(id: number, exitCode: number): void { + // Remove listeners + const process = this._terminalProcesses[id]; + process.removeAllListeners('message'); + process.removeAllListeners('exit'); + + // Remove process reference + delete this._terminalProcesses[id]; + + // Send exit event to main side + this._proxy.$sendProcessExit(id, exitCode); + } + private _getTerminalById(id: number): ExtHostTerminal { let index = this._getTerminalIndexById(id); return index !== null ? this._terminals[index] : null; @@ -176,6 +282,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _getTerminalIndexById(id: number): number { let index: number = null; this._terminals.some((terminal, i) => { + // TODO: This shouldn't be cas let thisId = (terminal)._id; if (thisId === id) { index = i; @@ -188,7 +295,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } class ApiRequest { - private _callback: (...args: any[]) => void; private _args: any[]; diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 299cf7327ed0776837930cdc58f64b78d9fd6a36..dfe9d89cc12a294c280fa9b9e2d74215e3e19376 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -103,26 +103,35 @@ export interface ITerminalFont { } export interface IShellLaunchConfig { - /** The name of the terminal, if this is not set the name of the process will be used. */ + /** + * The name of the terminal, if this is not set the name of the process will be used. + */ name?: string; - /** The shell executable (bash, cmd, etc.). */ + + /** + * The shell executable (bash, cmd, etc.). + */ executable?: string; + /** * The CLI arguments to use with executable, a string[] is in argv format and will be escaped, * a string is in "CommandLine" pre-escaped format and will be used as is. The string option is * only supported on Windows and will throw an exception if used on macOS or Linux. */ args?: string[] | string; + /** * The current working directory of the terminal, this overrides the `terminal.integrated.cwd` * settings key. */ cwd?: string; + /** * A custom environment for the terminal, if this is not set the environment will be inherited * from the VS Code process. */ env?: { [key: string]: string }; + /** * Whether to ignore a custom cwd from the `terminal.integrated.cwd` settings key (eg. if the * shell is being launched by an extension). @@ -151,6 +160,7 @@ export interface ITerminalService { onInstanceCreated: Event; onInstanceDisposed: Event; onInstanceProcessIdReady: Event; + onInstanceRequestExtHostProcess: Event; onInstancesChanged: Event; onInstanceTitleChanged: Event; terminalInstances: ITerminalInstance[]; @@ -191,6 +201,8 @@ export interface ITerminalService { setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; selectDefaultWindowsShell(): TPromise; setWorkspaceShellAllowed(isAllowed: boolean): void; + + requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void; } export const enum Direction { @@ -226,9 +238,10 @@ export interface ITerminalInstance { id: number; /** - * The process ID of the shell process. + * The process ID of the shell process, this is undefined when there is no process associated + * with this terminal. */ - processId: number; + processId: number | undefined; /** * An event that fires when the terminal instance's title changes. @@ -244,6 +257,8 @@ export interface ITerminalInstance { onProcessIdReady: Event; + onRequestExtHostProcess: Event; + processReady: TPromise; /** @@ -466,11 +481,6 @@ export interface ITerminalCommandTracker { selectToNextCommand(): void; } -export interface ITerminalProcessMessage { - type: 'pid' | 'data' | 'title'; - content: number | string; -} - export interface ITerminalProcessManager extends IDisposable { readonly processState: ProcessState; readonly ptyProcessReady: TPromise; @@ -506,3 +516,24 @@ export enum ProcessState { // was run. KILLED_BY_PROCESS } + + +export interface ITerminalProcessExtHostProxy extends IDisposable { + readonly terminalId: number; + + emitData(data: string): void; + emitTitle(title: string): void; + emitPid(pid: number): void; + emitExit(exitCode: number): void; + + onInput(listener: (data: string) => void): void; + onResize(listener: (cols: number, rows: number) => void): void; + onShutdown(listener: () => void): void; +} + +export interface ITerminalProcessExtHostRequest { + proxy: ITerminalProcessExtHostProxy; + shellLaunchConfig: IShellLaunchConfig; + cols: number; + rows: number; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index 69fcd5c98f4970bb06fc3cea412ee0a285a9cd87..e294388516b56a0f6e391c17a5813b0975d21530 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -9,7 +9,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -22,28 +22,31 @@ export abstract class TerminalService implements ITerminalService { protected _terminalFocusContextKey: IContextKey; protected _findWidgetVisible: IContextKey; protected _terminalContainer: HTMLElement; - protected _onInstancesChanged: Emitter; - protected _onTabDisposed: Emitter; - protected _onInstanceCreated: Emitter; - protected _onInstanceDisposed: Emitter; - protected _onInstanceProcessIdReady: Emitter; - protected _onInstanceTitleChanged: Emitter; protected _terminalTabs: ITerminalTab[]; protected abstract _terminalInstances: ITerminalInstance[]; private _activeTabIndex: number; - private readonly _onActiveTabChanged: Emitter; public get activeTabIndex(): number { return this._activeTabIndex; } + public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } + public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; } + + private readonly _onActiveTabChanged: Emitter = new Emitter(); public get onActiveTabChanged(): Event { return this._onActiveTabChanged.event; } - public get onTabDisposed(): Event { return this._onTabDisposed.event; } + protected readonly _onInstanceCreated: Emitter = new Emitter(); public get onInstanceCreated(): Event { return this._onInstanceCreated.event; } + protected readonly _onInstanceDisposed: Emitter = new Emitter(); public get onInstanceDisposed(): Event { return this._onInstanceDisposed.event; } + protected readonly _onInstanceProcessIdReady: Emitter = new Emitter(); public get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } - public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } + protected readonly _onInstanceRequestExtHostProcess: Emitter = new Emitter(); + public get onInstanceRequestExtHostProcess(): Event { return this._onInstanceRequestExtHostProcess.event; } + protected readonly _onInstancesChanged: Emitter = new Emitter(); public get onInstancesChanged(): Event { return this._onInstancesChanged.event; } - public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } - public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; } + protected readonly _onInstanceTitleChanged: Emitter = new Emitter(); + public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } + protected readonly _onTabDisposed: Emitter = new Emitter(); + public get onTabDisposed(): Event { return this._onTabDisposed.event; } public abstract get configHelper(): ITerminalConfigHelper; @@ -57,14 +60,6 @@ export abstract class TerminalService implements ITerminalService { this._activeTabIndex = 0; this._isShuttingDown = false; - this._onActiveTabChanged = new Emitter(); - this._onTabDisposed = new Emitter(); - this._onInstanceCreated = new Emitter(); - this._onInstanceDisposed = new Emitter(); - this._onInstanceProcessIdReady = new Emitter(); - this._onInstanceTitleChanged = new Emitter(); - this._onInstancesChanged = new Emitter(); - lifecycleService.onWillShutdown(event => event.veto(this._onWillShutdown())); lifecycleService.onShutdown(() => this._onShutdown()); this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); @@ -80,6 +75,7 @@ export abstract class TerminalService implements ITerminalService { public abstract getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance; public abstract selectDefaultWindowsShell(): TPromise; public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; + public abstract requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void; private _restoreTabs(): void { if (!this.configHelper.config.experimentalRestore) { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 11667f01d74918eeb3be8b6e2d3b6cc6442eaefc..d148d55861be1effcfb15e912cbc4c7119330a92 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -14,7 +14,7 @@ import * as platform from 'vs/base/common/platform'; import * as terminalCommands from 'vs/workbench/parts/terminal/common/terminalCommands'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE } from 'vs/workbench/parts/terminal/common/terminal'; -import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts index 243f7094819fb099a7edf155ecc43a7b9b27eaa9..a8e3f9ecbae15c58486995a64cabc8e37c40147c 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts @@ -12,7 +12,7 @@ import { IWorkspaceConfigurationService } from 'vs/workbench/services/configurat import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITerminalConfiguration, ITerminalConfigHelper, ITerminalFont, IShellLaunchConfig, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION } from 'vs/workbench/parts/terminal/common/terminal'; import Severity from 'vs/base/common/severity'; -import { isFedora } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { isFedora } from 'vs/workbench/parts/terminal/node/terminal'; import { Terminal as XTermTerminal } from 'vscode-xterm'; import { INotificationService } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 654333e1b3a05b6092b3820b5e980a455b54a7c5..0459ce481c8f590005cea325115ac44edc45dc94 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -71,24 +71,27 @@ export class TerminalInstance implements ITerminalInstance { public disableLayout: boolean; public get id(): number { return this._id; } // TODO: Ideally processId would be merged into processReady - public get processId(): number { return this._processManager.shellProcessId; } + public get processId(): number | undefined { return this._processManager ? this._processManager.shellProcessId : undefined; } + // TODO: How does this work with detached processes? // TODO: Should this be an event as it can fire twice? - public get processReady(): TPromise { return this._processManager.ptyProcessReady; } + public get processReady(): TPromise { return this._processManager ? this._processManager.ptyProcessReady : TPromise.as(void 0); } public get title(): string { return this._title; } public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } public get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; } public get shellLaunchConfig(): IShellLaunchConfig { return Object.freeze(this._shellLaunchConfig); } public get commandTracker(): TerminalCommandTracker { return this._commandTracker; } - private readonly _onDisposed: Emitter = new Emitter(); - private readonly _onFocused: Emitter = new Emitter(); - private readonly _onProcessIdReady: Emitter = new Emitter(); - private readonly _onTitleChanged: Emitter = new Emitter(); + private readonly _onDisposed: Emitter = new Emitter(); public get onDisposed(): Event { return this._onDisposed.event; } + private readonly _onFocused: Emitter = new Emitter(); public get onFocused(): Event { return this._onFocused.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onTitleChanged: Emitter = new Emitter(); public get onTitleChanged(): Event { return this._onTitleChanged.event; } + private readonly _onRequestExtHostProcess: Emitter = new Emitter(); + public get onRequestExtHostProcess(): Event { return this._onRequestExtHostProcess.event; } public constructor( private _terminalFocusContextKey: IContextKey, @@ -267,8 +270,9 @@ export class TerminalInstance implements ITerminalInstance { if (this._processManager) { this._processManager.onProcessData(data => this._sendPtyDataToXterm(data)); this._xterm.on('data', data => this._processManager.write(data)); + // TODO: How does the cwd work on detached processes? + this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager.initialCwd); } - this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager.initialCwd); this._commandTracker = new TerminalCommandTracker(this._xterm); this._disposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme))); } @@ -394,10 +398,13 @@ export class TerminalInstance implements ITerminalInstance { })); this._wrapperElement.appendChild(this._xtermElement); - this._widgetManager = new TerminalWidgetManager(this._wrapperElement); - this._linkHandler.setWidgetManager(this._widgetManager); this._container.appendChild(this._wrapperElement); + if (this._processManager) { + this._widgetManager = new TerminalWidgetManager(this._wrapperElement); + this._linkHandler.setWidgetManager(this._widgetManager); + } + const computedStyle = window.getComputedStyle(this._container); const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10); const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10); @@ -584,8 +591,7 @@ export class TerminalInstance implements ITerminalInstance { } protected _createProcess(): void { - // TODO: This should be injected in to the terminal instance (from service?) - this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._configHelper); + this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._id, this._configHelper); this._processManager.onProcessReady(() => this._onProcessIdReady.fire(this)); this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows); @@ -667,7 +673,11 @@ export class TerminalInstance implements ITerminalInstance { return a; }).join(' '); } - this._notificationService.error(nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode)); + if (this._shellLaunchConfig.executable) { + this._notificationService.error(nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode)); + } else { + this._notificationService.error(nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', exitCode)); + } } else { if (this._configHelper.config.showExitAlert) { this._notificationService.error(exitCodeMessage); @@ -868,7 +878,9 @@ export class TerminalInstance implements ITerminalInstance { } } - this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(this._cols, this._rows)); + if (this._processManager) { + this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(this._cols, this._rows)); + } } public setTitle(title: string, eventFromProcess: boolean): void { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts index b98130fa4683951a364c139941fe7ab5cb4e6fe0..1670b248a0b390cbe101448d012d914bb82a2a6a 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -4,18 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as cp from 'child_process'; -import * as path from 'path'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; import Uri from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { ProcessState, ITerminalProcessManager, ITerminalProcessMessage, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; +import { ProcessState, ITerminalProcessManager, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { ITerminalChildProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { TerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/node/terminalProcessExtHostProxy'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -34,7 +36,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { public shellProcessId: number; public initialCwd: string; - private _process: cp.ChildProcess; + private _process: ITerminalChildProcess; private _preLaunchInputQueue: string[] = []; private _disposables: IDisposable[] = []; @@ -48,12 +50,20 @@ export class TerminalProcessManager implements ITerminalProcessManager { public get onProcessExit(): Event { return this._onProcessExit.event; } constructor( + private _terminalId: number, private _configHelper: ITerminalConfigHelper, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IHistoryService private readonly _historyService: IHistoryService, @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private _logService: ILogService ) { + this.ptyProcessReady = new TPromise(c => { + this.onProcessReady(() => { + this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`); + c(void 0); + }); + }); } public dispose(): void { @@ -80,39 +90,38 @@ export class TerminalProcessManager implements ITerminalProcessManager { cols: number, rows: number ): void { - this.ptyProcessReady = new TPromise(c => { - this.onProcessReady(() => { - this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`); - c(void 0); - }); - }); + const extensionHostOwned = (this._configHelper.config).extHostProcess; + if (extensionHostOwned) { + this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, cols, rows); + } else { + const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; + if (!shellLaunchConfig.executable) { + this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); + } - const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; - if (!shellLaunchConfig.executable) { - this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig); - } + const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); + this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); - const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); - this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); - - // Resolve env vars from config and shell - const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); - const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); - const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); - const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); - shellLaunchConfig.env = envFromShell; - - // Merge process env with the env from config - const parentEnv = { ...process.env }; - terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); - - // Continue env initialization, merging in the env from the launch - // config and adding keys that are needed to create the process - const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows); - const cwd = Uri.parse(path.dirname(require.toUrl('../node/terminalProcess'))).fsPath; - const options = { env, cwd }; - this._logService.debug(`Terminal process launching`, options); - this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + // Resolve env vars from config and shell + const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri); + const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot); + const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot); + shellLaunchConfig.env = envFromShell; + + // Merge process env with the env from config + const parentEnv = { ...process.env }; + terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + + // Continue env initialization, merging in the env from the launch + // config and adding keys that are needed to create the process + const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows); + const cwd = Uri.parse(require.toUrl('../node')).fsPath; + const options = { env, cwd }; + this._logService.debug(`Terminal process launching`, options); + + this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + } this.processState = ProcessState.LAUNCHING; this._process.on('message', message => this._onMessage(message)); @@ -152,7 +161,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { } } - private _onMessage(message: ITerminalProcessMessage): void { + private _onMessage(message: IMessageFromTerminalProcess): void { this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message); switch (message.type) { case 'data': diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index c99a2433259c9721ca50de2b964b74c56872c053..1ee8373b6430df0e4b164c99b4b0450d77eb0a8c 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -13,13 +13,13 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IQuickOpenService, IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; -import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfigHelper, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfigHelper, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, TERMINAL_PANEL_ID, ITerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/common/terminal'; import { TerminalService as AbstractTerminalService } from 'vs/workbench/parts/terminal/common/terminalService'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; import { TPromise } from 'vs/base/common/winjs.base'; import Severity from 'vs/base/common/severity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal'; import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel'; import { TerminalTab } from 'vs/workbench/parts/terminal/browser/terminalTab'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -27,6 +27,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { ipcRenderer as ipc } from 'electron'; import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class TerminalService extends AbstractTerminalService implements ITerminalService { private _configHelper: TerminalConfigHelper; @@ -47,7 +48,8 @@ export class TerminalService extends AbstractTerminalService implements ITermina @IInstantiationService private readonly _instantiationService: IInstantiationService, @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, @INotificationService private readonly _notificationService: INotificationService, - @IDialogService private readonly _dialogService: IDialogService + @IDialogService private readonly _dialogService: IDialogService, + @IExtensionService private readonly _extensionService: IExtensionService ) { super(contextKeyService, panelService, partService, lifecycleService, storageService); @@ -90,7 +92,17 @@ export class TerminalService extends AbstractTerminalService implements ITermina } public createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance { - return this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, undefined, shellLaunchConfig, true); + return this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig, true); + } + + public requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void { + // Ensure extension host is ready before requesting a process + this._extensionService.whenInstalledExtensionsRegistered().then(() => { + // TODO: MainThreadTerminalService is not ready at this point, fix this + setTimeout(() => { + this._onInstanceRequestExtHostProcess.fire({ proxy, shellLaunchConfig, cols, rows }); + }, 500); + }); } public focusFindWidget(): TPromise { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.ts b/src/vs/workbench/parts/terminal/node/terminal.ts similarity index 74% rename from src/vs/workbench/parts/terminal/electron-browser/terminal.ts rename to src/vs/workbench/parts/terminal/node/terminal.ts index e6d1478907d98cf61459b83f095a88432ae10a38..8cb9bc9dddd25bd8fbf0c03070f0d98ad75d72f8 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.ts +++ b/src/vs/workbench/parts/terminal/node/terminal.ts @@ -8,6 +8,31 @@ import * as platform from 'vs/base/common/platform'; import * as processes from 'vs/base/node/processes'; import { readFile, fileExists } from 'vs/base/node/pfs'; +export interface IMessageFromTerminalProcess { + type: 'pid' | 'data' | 'title'; + content: number | string; +} + +export interface IMessageToTerminalProcess { + event: 'resize' | 'input' | 'shutdown'; + data?: string; + cols?: number; + rows?: number; +} + +/** + * An interface representing a raw terminal child process, this is a subset of the + * child_process.ChildProcess node.js interface. + */ +export interface ITerminalChildProcess { + readonly connected: boolean; + + send(message: IMessageToTerminalProcess): boolean; + + on(event: 'exit', listener: (code: number) => void): this; + on(event: 'message', listener: (message: IMessageFromTerminalProcess) => void): this; +} + let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string = null; export function getTerminalDefaultShellUnixLike(): string { if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) { @@ -50,4 +75,4 @@ if (platform.isLinux) { }); } -export let isFedora = false; +export let isFedora = false; \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts index e52554048e2651c320a9cb3f28de255e21c309c8..2c60754603da79e4076fc8b9c1d0e08f5c63cf65 100644 --- a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts @@ -79,7 +79,6 @@ export function createTerminalEnv(parentEnv: IStringDictionary, shell: I return env; } -// TODO:should be protected/non-static export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary { Object.keys(env).forEach((key) => { if (typeof env[key] === 'string') { diff --git a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c842fbbfa8cd683a8115a5c32af2bdaea08858a --- /dev/null +++ b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITerminalChildProcess, IMessageToTerminalProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { EventEmitter } from 'events'; +import { ITerminalService, ITerminalProcessExtHostProxy, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; + +export class TerminalProcessExtHostProxy extends EventEmitter implements ITerminalChildProcess, ITerminalProcessExtHostProxy { + // For ext host processes connected checks happen on the ext host + public connected: boolean = true; + + private _disposables: IDisposable[] = []; + + constructor( + public terminalId: number, + shellLaunchConfig: IShellLaunchConfig, + cols: number, + rows: number, + @ITerminalService private _terminalService: ITerminalService + ) { + super(); + + // TODO: Return TPromise indicating success? Teardown if failure? + this._terminalService.requestExtHostProcess(this, shellLaunchConfig, cols, rows); + } + + public dispose(): void { + this._disposables.forEach(d => d.dispose()); + this._disposables.length = 0; + } + + public emitData(data: string): void { + this.emit('message', { type: 'data', content: data } as IMessageFromTerminalProcess); + } + + public emitTitle(title: string): void { + this.emit('message', { type: 'title', content: title } as IMessageFromTerminalProcess); + } + + public emitPid(pid: number): void { + this.emit('message', { type: 'pid', content: pid } as IMessageFromTerminalProcess); + } + + public emitExit(exitCode: number): void { + this.emit('exit', exitCode); + this.dispose(); + } + + public send(message: IMessageToTerminalProcess): boolean { + switch (message.event) { + case 'input': this.emit('input', message.data); break; + case 'resize': this.emit('resize', message.cols, message.rows); break; + case 'shutdown': this.emit('shutdown'); break; + } + return true; + } + + public onInput(listener: (data: string) => void): void { + const outerListener = (data) => listener(data); + this.on('input', outerListener); + this._disposables.push(toDisposable(() => this.removeListener('input', outerListener))); + } + + public onResize(listener: (cols: number, rows: number) => void): void { + const outerListener = (cols, rows) => listener(cols, rows); + this.on('resize', outerListener); + this._disposables.push(toDisposable(() => this.removeListener('resize', outerListener))); + } + + public onShutdown(listener: () => void): void { + const outerListener = () => listener(); + this.on('shutdown', outerListener); + this._disposables.push(toDisposable(() => this.removeListener('shutdown', outerListener))); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts index 4a37a7ae84c2a805247733797812ae4dce001999..cc4cec690c45ca1ed66b6ba137b98ddcfda5d046 100644 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; -import { isFedora } from 'vs/workbench/parts/terminal/electron-browser/terminal'; +import { isFedora } from 'vs/workbench/parts/terminal/node/terminal'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; suite('Workbench - TerminalConfigHelper', () => {