diff --git a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index 5f574cedd7118a134d234b15d575588563a5ef99..253fb655b65cdd36d8030fe1e1b27ecd7ff83583 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -5,7 +5,7 @@ '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 } 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 { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @@ -14,23 +14,24 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC 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 ) { + console.log('MainThreadTerminalService#ctor'); 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.onInstanceRequestExtHostProcess((terminalInstance) => this._onTerminalRequestExtHostProcess(terminalInstance))); + this._toDispose.push(terminalService.onInstanceDisposed(terminalInstance => this._onTerminalDisposed(terminalInstance))); + this._toDispose.push(terminalService.onInstanceProcessIdReady(terminalInstance => this._onTerminalProcessIdReady(terminalInstance))); + this._toDispose.push(terminalService.onInstanceRequestExtHostProcess(proxy => this._onTerminalRequestExtHostProcess(proxy))); // Set initial ext host state this.terminalService.terminalInstances.forEach(t => { @@ -99,8 +100,27 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalProcessId(terminalInstance.id, terminalInstance.processId); } - private _onTerminalRequestExtHostProcess(terminalInstance: ITerminalInstance): void { + private _onTerminalRequestExtHostProcess(proxy: ITerminalProcessExtHostProxy): void { console.log('mainThreadTerminalService#_onTerminalRequestExtHostProcess', arguments); - this._proxy.$createProcess(null, 0, 0); + this._terminalProcesses[proxy.terminalId] = proxy; + this._proxy.$createProcess(proxy.terminalId, null, 0, 0); + // TODO: Dispose of this properly when the terminal/process dies + this._toDispose.push(proxy.onInput(data => this._onTerminalProcessWrite(proxy.terminalId, data))); + } + + 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); + } + + private _onTerminalProcessWrite(terminalId: number, data: string): void { + this._proxy.$acceptTerminalProcessWrite(terminalId, data); } } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index ce3753c84e77e8e4e55b1e2aec22fbcfa8877af2..619181eff1fc498f3439a1455e2ba9a36490da43 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -321,6 +321,10 @@ 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; } export interface MyQuickPickItems extends IPickOpenEntry { @@ -743,7 +747,8 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number): void; $acceptTerminalOpened(id: number, name: string): void; $acceptTerminalProcessId(id: number, processId: number): void; - $createProcess(shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void; + $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void; + $acceptTerminalProcessWrite(id: number, data: string): void; } export interface ExtHostSCMShape { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index c1632fc7629e3795ddbf1bc3ce6a433efc1292fa..9310283c71effa09d990bdc42ef7804abf9dd8b9 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -5,8 +5,12 @@ 'use strict'; import * as vscode from 'vscode'; +import * as cp from 'child_process'; +import * as path from 'path'; +import * as terminalEnvironment from 'vs/workbench/parts/terminal/node/terminalEnvironment'; import { Event, Emitter } from 'vs/base/common/event'; import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IMainContext, ShellLaunchConfigDto } from 'vs/workbench/api/node/extHost.protocol'; +import { IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; export class ExtHostTerminal implements vscode.Terminal { @@ -81,6 +85,7 @@ export class ExtHostTerminal implements vscode.Terminal { } public _setProcessId(processId: number): void { + console.log('extHostTerminalService#_setProcessId', processId); this._pidPromiseComplete(processId); this._pidPromiseComplete = null; } @@ -106,7 +111,8 @@ 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; } @@ -114,7 +120,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { 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 { @@ -150,6 +155,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } public $acceptTerminalOpened(id: number, name: string): void { + console.log('terminal opened: ' + id); let index = this._getTerminalIndexById(id); if (index !== null) { // The terminal has already been created (via createTerminal*), only fire the event @@ -163,13 +169,64 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { public $acceptTerminalProcessId(id: number, processId: number): void { let terminal = this._getTerminalById(id); + console.log('ExtHostTerminalService#$acceptTerminalProcessId ' + id + ' ' + processId); if (terminal) { terminal._setProcessId(processId); } } - public $createProcess(shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void { - console.log('$createProcess'); + public $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void { + shellLaunchConfig = { + env: {}, + executable: 'bash' + }; + + + // TODO: Launch process + // TODO: Associate the process with the terminal object/id + // TODO: terminal has incorrect name/options, fix up + const parentEnv = { ...process.env }; + const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, '/home/daniel', undefined, cols, rows); + // TODO: Use Uri? + let cwd = path.dirname(require.toUrl('../../parts/terminal/node/terminalProcess')).replace('file://', ''); + console.log(cwd); + const options = { env, cwd, execArgv: [] }; + + let bootstrapUri = require.toUrl('bootstrap').replace('file://', '') + '.js'; + console.log(bootstrapUri); + + // cwd = '/home/daniel/dev/Microsoft/vscode/out/vs/workbench/parts/terminal/node'; + // bootstrapUri = '/home/daniel/dev/Microsoft/vscode/out/bootstrap'; + // env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess'; + this._terminalProcesses[id] = cp.fork(bootstrapUri, ['--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; + } + // console.log('message type: ' + d.type + ', content: ' + d.content) + }); + + // const processPath = require.toUrl('../../parts/terminal/node/terminalProcess').replace('file://', ''); + // const process2 = cp.fork(processPath, [], options); + // process2.on('data', d => console.log('data ' + d)); + // process2.on('exit', d => console.log('exit ' + d)); + // process2.on('error', d => console.log('error ' + d)); + + const terminal = this._getTerminalById(id); + console.log('$createProcess terminal: ' + terminal.name); + } + + public $acceptTerminalProcessWrite(id: number, data: string): void { + this._terminalProcesses[id].send({ event: 'input', data }); } private _getTerminalById(id: number): ExtHostTerminal { @@ -180,6 +237,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; diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index d4934907a46cf823f90fe31decb09e52fbf1fbc7..a435e65df32642491a71ff445c520d38a28824c8 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -165,7 +165,7 @@ export interface ITerminalService { onInstanceCreated: Event; onInstanceDisposed: Event; onInstanceProcessIdReady: Event; - onInstanceRequestExtHostProcess: Event; + onInstanceRequestExtHostProcess: Event; onInstancesChanged: Event; onInstanceTitleChanged: Event; terminalInstances: ITerminalInstance[]; @@ -207,7 +207,7 @@ export interface ITerminalService { selectDefaultWindowsShell(): TPromise; setWorkspaceShellAllowed(isAllowed: boolean): void; - requestExtHostProcess(proxy: ITerminalProcessExtHostProxy): TPromise; + requestExtHostProcess(proxy: ITerminalProcessExtHostProxy): void; } export const enum Direction { @@ -524,7 +524,10 @@ export enum ProcessState { export interface ITerminalProcessExtHostProxy { + readonly terminalId: number; + emitData(data: string): void; emitTitle(title: string): void; emitPid(pid: number): void; -} \ No newline at end of file + onInput(listener: (data: string) => void): IDisposable; +} diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index 2ec8be7c6bb510fea7960af9df74d023693a7ad8..92a91bcf5ac054e0844f16761b6413f2f189c384 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -39,8 +39,8 @@ export abstract class TerminalService implements ITerminalService { public get onInstanceDisposed(): Event { return this._onInstanceDisposed.event; } protected readonly _onInstanceProcessIdReady: Emitter = new Emitter(); public get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } - protected readonly _onInstanceRequestExtHostProcess: Emitter = new Emitter(); - public get onInstanceRequestExtHostProcess(): Event { return this._onInstanceRequestExtHostProcess.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; } protected readonly _onInstanceTitleChanged: Emitter = new Emitter(); @@ -75,7 +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): TPromise; + public abstract requestExtHostProcess(proxy: ITerminalProcessExtHostProxy): void; private _restoreTabs(): void { if (!this.configHelper.config.experimentalRestore) { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 37f4a2168829312399eab0dd22ec418d04f5ad1f..cfbbcd194b337a10149c524a68924ea350f210ce 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -587,7 +587,7 @@ export class TerminalInstance implements ITerminalInstance { } protected _createProcess(): void { - 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); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts index 9d3c4ba3a4c80b37fdc853f2c09e06bb3df29a42..bfeef947e408f18c1e5bebb637cf2c7854a1f0fa 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -51,6 +51,7 @@ 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, @@ -117,7 +118,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { const options = { env, cwd }; this._logService.debug(`Terminal process launching`, options); if (shellLaunchConfig.extensionHostOwned) { - this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy); + this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId); } else { this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index 470d17dcb0d6ca5ccf4746b43ecd63a17f63e7eb..0e37f567c636a9335c69c44de9d82423369fbb17 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -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 '../../../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); @@ -93,17 +95,15 @@ export class TerminalService extends AbstractTerminalService implements ITermina return this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, undefined, shellLaunchConfig, true); } - public requestExtHostProcess(proxy: ITerminalProcessExtHostProxy): TPromise { - let i = 0; - setTimeout(() => { - proxy.emitPid(-1); - proxy.emitTitle('test title'); - proxy.emitData(`test ${i++}\r\n`); - }, 0); - setInterval(() => { - proxy.emitData(`test ${i++}\r\n`); - }, 1000); - return TPromise.as(void 0); + public requestExtHostProcess(proxy: ITerminalProcessExtHostProxy): 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(() => { + console.log('request'); + this._onInstanceRequestExtHostProcess.fire(proxy); + }, 100); + }); } public focusFindWidget(): TPromise { diff --git a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts index 271e8b0c54393f4e9677dcadb10a22822f7da5e5..d59cb65253463a5ed6b5f1af610d2c6f79d5d924 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts @@ -6,17 +6,19 @@ import { ITerminalChildProcess, IMessageToTerminalProcess, IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; import { EventEmitter } from 'events'; import { ITerminalService, ITerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/common/terminal'; +import { IDisposable } from '../../../../base/common/lifecycle'; export class TerminalProcessExtHostProxy extends EventEmitter implements ITerminalChildProcess, ITerminalProcessExtHostProxy { public connected: boolean; constructor( + public terminalId: number, @ITerminalService private _terminalService: ITerminalService ) { super(); - this._terminalService.requestExtHostProcess(this).then(() => { - }); + // TODO: Return TPromise indicating success? Teardown if failure? + this._terminalService.requestExtHostProcess(this); } public emitData(data: string): void { @@ -30,7 +32,21 @@ export class TerminalProcessExtHostProxy extends EventEmitter implements ITermin } public send(message: IMessageToTerminalProcess): boolean { - console.log('TerminalProcessExtHostBridge#send', arguments); + console.log('TerminalProcessExtHostProxy#send'); + if (message.event === 'input') { + console.log('emit input', message.data); + this.emit('input', message.data); + } return true; } + + public onInput(listener: (data: string) => void): IDisposable { + console.log('TerminalProcessExtHostProxy#onInput', arguments); + // TODO: Dispose of me + this.on('input', data => { + console.log('TerminalProcessExtHostProxy#onInput - listener'); + listener(data); + }); + return null; + } } \ No newline at end of file