diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts index 64a26d1017cd6..1baae6f64c2f2 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; +// import * as cp from 'child_process'; 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'; @@ -15,9 +15,10 @@ 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 { 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'; +import { TerminalProcess } from 'vs/workbench/parts/terminal/node/terminalProcess'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -36,7 +37,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { public shellProcessId: number; public initialCwd: string; - private _process: ITerminalChildProcess; + private _process: TerminalProcess; private _preLaunchInputQueue: string[] = []; private _disposables: IDisposable[] = []; @@ -58,6 +59,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private _logService: ILogService ) { + console.log(this._terminalId, this._instantiationService); this.ptyProcessReady = new TPromise(c => { this.onProcessReady(() => { this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`); @@ -68,12 +70,13 @@ export class TerminalProcessManager implements ITerminalProcessManager { public dispose(): void { if (this._process) { - if (this._process.connected) { + if (this._process.isConnected) { // If the process was still connected this dispose came from // within VS Code, not the process, so mark the process as // killed by the user. this.processState = ProcessState.KILLED_BY_USER; - this._process.send({ event: 'shutdown' }); + this._process.shutdown(); + // this._process.send({ event: 'shutdown' }); } this._process = null; } @@ -90,42 +93,60 @@ export class TerminalProcessManager implements ITerminalProcessManager { cols: number, rows: number ): void { - 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 lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file'); - this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper); + // 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); + } - // 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; + 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(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._process = new TerminalProcess(env['PTYSHELL'], [], env['PTYCWD'], cols, rows); + // } + this.processState = ProcessState.LAUNCHING; - // Merge process env with the env from config - const parentEnv = { ...process.env }; - terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig); + this._process.onData(data => { + this._onProcessData.fire(data); + }); - // 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.onProcessIdReady(pid => { + this.shellProcessId = pid; + this._onProcessReady.fire(); - this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); - } - this.processState = ProcessState.LAUNCHING; + // Send any queued data that's waiting + if (this._preLaunchInputQueue.length > 0) { + this._process.input(this._preLaunchInputQueue.join('')); + this._preLaunchInputQueue.length = 0; + } + }); - this._process.on('message', message => this._onMessage(message)); - this._process.on('exit', exitCode => this._onExit(exitCode)); + this._process.onTitleChanged(title => this._onProcessTitle.fire(title)); + this._process.onExit(exitCode => this._onExit(exitCode)); + // this._process.on('message', message => this._onMessage(message)); + // this._process.on('exit', exitCode => this._onExit(exitCode)); setTimeout(() => { if (this.processState === ProcessState.LAUNCHING) { @@ -135,10 +156,11 @@ export class TerminalProcessManager implements ITerminalProcessManager { } public setDimensions(cols: number, rows: number): void { - if (this._process && this._process.connected) { + if (this._process && this._process.isConnected) { // The child process could aready be terminated try { - this._process.send({ event: 'resize', cols, rows }); + this._process.resize(cols, rows); + // this._process.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') { @@ -152,10 +174,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { if (this.shellProcessId) { if (this._process) { // Send data if the pty is ready - this._process.send({ - event: 'input', - data - }); + this._process.input(data); } } else { // If the pty is not ready, queue the data received to send later @@ -163,30 +182,30 @@ export class TerminalProcessManager implements ITerminalProcessManager { } } - private _onMessage(message: IMessageFromTerminalProcess): void { - this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message); - switch (message.type) { - case 'data': - this._onProcessData.fire(message.content); - break; - case 'pid': - this.shellProcessId = message.content; - this._onProcessReady.fire(); - - // Send any queued data that's waiting - if (this._preLaunchInputQueue.length > 0) { - this._process.send({ - event: 'input', - data: this._preLaunchInputQueue.join('') - }); - this._preLaunchInputQueue.length = 0; - } - break; - case 'title': - this._onProcessTitle.fire(message.content); - break; - } - } + // private _onMessage(message: IMessageFromTerminalProcess): void { + // this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message); + // switch (message.type) { + // case 'data': + // this._onProcessData.fire(message.content); + // break; + // case 'pid': + // this.shellProcessId = message.content; + // this._onProcessReady.fire(); + + // // Send any queued data that's waiting + // if (this._preLaunchInputQueue.length > 0) { + // this._process.send({ + // event: 'input', + // data: this._preLaunchInputQueue.join('') + // }); + // this._preLaunchInputQueue.length = 0; + // } + // break; + // case 'title': + // this._onProcessTitle.fire(message.content); + // break; + // } + // } private _onExit(exitCode: number): void { this._process = null; diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index 8987b35804958..f5d27ed7dcf22 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -6,162 +6,210 @@ import * as os from 'os'; import * as path from 'path'; import * as pty from 'node-pty'; +import { Event, Emitter } from 'vs/base/common/event'; + +export class TerminalProcess { + private _exitCode: number; + private _closeTimeout: number; + private _ptyProcess: pty.IPty; + private _currentTitle: string = ''; + + private readonly _onData: Emitter = new Emitter(); + public get onData(): Event { return this._onData.event; } + private readonly _onExit: Emitter = new Emitter(); + public get onExit(): Event { return this._onExit.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; } + + constructor(shell: string, args: string | string[], cwd: string, cols: number, rows: number) { + // The pty process needs to be run in its own child process to get around maxing out CPU on Mac, + // see https://github.com/electron/electron/issues/38 + let shellName: string; + if (os.platform() === 'win32') { + shellName = path.basename(process.env.PTYSHELL); + } else { + // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a + // color prompt as defined in the default ~/.bashrc file. + shellName = 'xterm-256color'; + } + // const shell = process.env.PTYSHELL; + // const args = getArgs(); + // const cwd = process.env.PTYCWD; + // const cols = process.env.PTYCOLS; + // const rows = process.env.PTYROWS; + // let currentTitle = ''; + + // setupPlanB(Number(process.env.PTYPID)); + cleanEnv(); + + interface IOptions { + name: string; + cwd: string; + cols?: number; + rows?: number; + } -// The pty process needs to be run in its own child process to get around maxing out CPU on Mac, -// see https://github.com/electron/electron/issues/38 -let shellName: string; -if (os.platform() === 'win32') { - shellName = path.basename(process.env.PTYSHELL); -} else { - // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a - // color prompt as defined in the default ~/.bashrc file. - shellName = 'xterm-256color'; -} -const shell = process.env.PTYSHELL; -const args = getArgs(); -const cwd = process.env.PTYCWD; -const cols = process.env.PTYCOLS; -const rows = process.env.PTYROWS; -let currentTitle = ''; - -setupPlanB(Number(process.env.PTYPID)); -cleanEnv(); - -interface IOptions { - name: string; - cwd: string; - cols?: number; - rows?: number; -} - -const options: IOptions = { - name: shellName, - cwd -}; -if (cols && rows) { - options.cols = parseInt(cols, 10); - options.rows = parseInt(rows, 10); -} - -const ptyProcess = pty.spawn(shell, args, options); + const options: IOptions = { + name: shellName, + cwd + }; + if (cols && rows) { + // options.cols = parseInt(cols, 10); + // options.rows = parseInt(rows, 10); + options.cols = cols; + options.rows = rows; + } -let closeTimeout: number; -let exitCode: number; + const ptyProcess = pty.spawn(shell, args, options); + this._ptyProcess = ptyProcess; + + // let closeTimeout: number; + // let exitCode: number; + + (ptyProcess).on('data-buffered', (data) => { + this._onData.fire(data); + // process.send({ + // type: 'data', + // content: data + // }); + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + this._queueProcessExit(); + } + }); + + ptyProcess.on('exit', (code) => { + this._exitCode = code; + this._queueProcessExit(); + }); + + // process.on('message', (message) => { + // if (message.event === 'input') { + // ptyProcess.write(message.data); + // } else if (message.event === 'resize') { + // // Ensure that cols and rows are always >= 1, this prevents a native + // // exception in winpty. + // ptyProcess.resize(Math.max(message.cols, 1), Math.max(message.rows, 1)); + // } else if (message.event === 'shutdown') { + // this._queueProcessExit(); + // } + // }); + + setTimeout(() => { + this._sendProcessId(); + }, 1000); + this._setupTitlePolling(); + + // function getArgs(): string | string[] { + // if (process.env['PTYSHELLCMDLINE']) { + // return process.env['PTYSHELLCMDLINE']; + // } + // const args = []; + // let i = 0; + // while (process.env['PTYSHELLARG' + i]) { + // args.push(process.env['PTYSHELLARG' + i]); + // i++; + // } + // return args; + // } + + function cleanEnv() { + const keys = [ + 'AMD_ENTRYPOINT', + 'ELECTRON_NO_ASAR', + 'ELECTRON_RUN_AS_NODE', + 'GOOGLE_API_KEY', + 'PTYCWD', + 'PTYPID', + 'PTYSHELL', + 'PTYCOLS', + 'PTYROWS', + 'PTYSHELLCMDLINE', + 'VSCODE_LOGS', + 'VSCODE_PORTABLE', + 'VSCODE_PID', + ]; + // TODO: Don't change process.env, create a new one + keys.forEach(function (key) { + if (process.env[key]) { + delete process.env[key]; + } + }); + let i = 0; + while (process.env['PTYSHELLARG' + i]) { + delete process.env['PTYSHELLARG' + i]; + i++; + } + } -// Allow any trailing data events to be sent before the exit event is sent. -// See https://github.com/Tyriar/node-pty/issues/72 -function queueProcessExit() { - if (closeTimeout) { - clearTimeout(closeTimeout); + // function setupPlanB(parentPid: number) { + // setInterval(function () { + // try { + // process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore. + // } catch (e) { + // process.exit(); + // } + // }, 5000); + // } } - closeTimeout = setTimeout(function () { - ptyProcess.kill(); - process.exit(exitCode); - }, 250); -} -ptyProcess.on('data', function (data) { - process.send({ - type: 'data', - content: data - }); - if (closeTimeout) { - clearTimeout(closeTimeout); - queueProcessExit(); + private _setupTitlePolling() { + this._sendProcessTitle(); + setInterval(() => { + if (this._currentTitle !== this._ptyProcess.process) { + this._sendProcessTitle(); + } + }, 200); } -}); -ptyProcess.on('exit', function (code) { - exitCode = code; - queueProcessExit(); -}); - -process.on('message', function (message) { - if (message.event === 'input') { - ptyProcess.write(message.data); - } else if (message.event === 'resize') { - // Ensure that cols and rows are always >= 1, this prevents a native - // exception in winpty. - ptyProcess.resize(Math.max(message.cols, 1), Math.max(message.rows, 1)); - } else if (message.event === 'shutdown') { - queueProcessExit(); + // Allow any trailing data events to be sent before the exit event is sent. + // See https://github.com/Tyriar/node-pty/issues/72 + private _queueProcessExit() { + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + } + // TODO: Dispose correctly + this._closeTimeout = setTimeout(() => { + this._ptyProcess.kill(); + this._onExit.fire(this._exitCode); + // process.exit(exitCode); + }, 250); } -}); - -sendProcessId(); -setupTitlePolling(); -function getArgs(): string | string[] { - if (process.env['PTYSHELLCMDLINE']) { - return process.env['PTYSHELLCMDLINE']; + private _sendProcessId() { + this._onProcessIdReady.fire(this._ptyProcess.pid); + // process.send({ + // type: 'pid', + // content: ptyProcess.pid + // }); } - const args = []; - let i = 0; - while (process.env['PTYSHELLARG' + i]) { - args.push(process.env['PTYSHELLARG' + i]); - i++; + private _sendProcessTitle(): void { + // process.send({ + // type: 'title', + // content: ptyProcess.process + // }); + this._currentTitle = this._ptyProcess.process; + this._onTitleChanged.fire(this._currentTitle); } - return args; -} -function cleanEnv() { - const keys = [ - 'AMD_ENTRYPOINT', - 'ELECTRON_NO_ASAR', - 'ELECTRON_RUN_AS_NODE', - 'GOOGLE_API_KEY', - 'PTYCWD', - 'PTYPID', - 'PTYSHELL', - 'PTYCOLS', - 'PTYROWS', - 'PTYSHELLCMDLINE', - 'VSCODE_LOGS', - 'VSCODE_PORTABLE', - 'VSCODE_PID', - ]; - keys.forEach(function (key) { - if (process.env[key]) { - delete process.env[key]; - } - }); - let i = 0; - while (process.env['PTYSHELLARG' + i]) { - delete process.env['PTYSHELLARG' + i]; - i++; + public shutdown(): void { + this._queueProcessExit(); } -} -function setupPlanB(parentPid: number) { - setInterval(function () { - try { - process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore. - } catch (e) { - process.exit(); - } - }, 5000); -} - -function sendProcessId() { - process.send({ - type: 'pid', - content: ptyProcess.pid - }); -} + public input(data: string): void { + this._ptyProcess.write(data); + } -function setupTitlePolling() { - sendProcessTitle(); - setInterval(function () { - if (currentTitle !== ptyProcess.process) { - sendProcessTitle(); - } - }, 200); -} + public resize(cols: number, rows: number): void { + // Ensure that cols and rows are always >= 1, this prevents a native + // exception in winpty. + this._ptyProcess.resize(Math.max(cols, 1), Math.max(rows, 1)); + } -function sendProcessTitle() { - process.send({ - type: 'title', - content: ptyProcess.process - }); - currentTitle = ptyProcess.process; + public get isConnected(): boolean { + // Don't need connected anymore as it's the same process + return true; + } }