diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 4831c70e07bfb1b57b1d1d0d3bf6fba67d8d5538..71c9530041be5c24cbd84bec7e11390f48f19d83 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -78,7 +78,6 @@ const vscodeResources = [ 'out-build/vs/workbench/parts/webview/electron-browser/webview-pre.js', 'out-build/vs/**/markdown.css', 'out-build/vs/workbench/parts/tasks/**/*.json', - 'out-build/vs/workbench/parts/terminal/electron-browser/terminalProcess.js', 'out-build/vs/workbench/parts/welcome/walkThrough/**/*.md', 'out-build/vs/workbench/services/files/**/*.exe', 'out-build/vs/workbench/services/files/**/*.md', diff --git a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index 242091da9b19d6b1e451e5b1bd0777c96e28af31..ccf129ce117147e4dd8e93792ee4b88f493df99b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -178,7 +178,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape }; 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.onResize(dimensions => this._proxy.$acceptProcessResize(request.proxy.terminalId, dimensions.cols, dimensions.rows)); request.proxy.onShutdown(() => this._proxy.$acceptProcessShutdown(request.proxy.terminalId)); } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 956f724057e6cf982154a9fb87324c580a928a50..55ecfbfe88dcfb6c0c264c17ee78eb89d39e494a 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -5,17 +5,15 @@ '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, 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'; import { EXT_HOST_CREATION_DELAY } from 'vs/workbench/parts/terminal/common/terminal'; +import { TerminalProcess } from 'vs/workbench/parts/terminal/node/terminalProcess'; const RENDERER_NO_PROCESS_ID = -1; @@ -226,7 +224,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _proxy: MainThreadTerminalServiceShape; private _activeTerminal: ExtHostTerminal; private _terminals: ExtHostTerminal[] = []; - private _terminalProcesses: { [id: number]: cp.ChildProcess } = {}; + private _terminalProcesses: { [id: number]: TerminalProcess } = {}; private _terminalRenderers: ExtHostTerminalRenderer[] = []; public get activeTerminal(): ExtHostTerminal { return this._activeTerminal; } @@ -359,7 +357,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { 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); @@ -383,61 +380,48 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // 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); + const env = { ...process.env }; + // terminalEnvironment.mergeEnvironments(env, envFromConfig); + terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); // 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); - const cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath; - const options = { env, cwd, execArgv: [] }; + const locale = terminalConfig.get('setLocaleVariables') ? platform.locale : undefined; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); // 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)); + this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env); + this._terminalProcesses[id] = new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env); + this._terminalProcesses[id].onProcessIdReady(pid => this._proxy.$sendProcessPid(id, pid)); + this._terminalProcesses[id].onProcessTitleChanged(title => this._proxy.$sendProcessTitle(id, title)); + this._terminalProcesses[id].onProcessData(data => this._proxy.$sendProcessData(id, data)); + this._terminalProcesses[id].onProcessExit((exitCode) => this._onProcessExit(id, exitCode)); } public $acceptProcessInput(id: number, data: string): void { - if (this._terminalProcesses[id].connected) { - this._terminalProcesses[id].send({ event: 'input', data }); - } + this._terminalProcesses[id].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); - } + try { + this._terminalProcesses[id].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' }); - } + this._terminalProcesses[id].shutdown(); } private _onProcessExit(id: number, exitCode: number): void { // Remove listeners - const process = this._terminalProcesses[id]; - process.removeAllListeners('message'); - process.removeAllListeners('exit'); + this._terminalProcesses[id].dispose(); // Remove process reference delete this._terminalProcesses[id]; diff --git a/src/vs/workbench/buildfile.js b/src/vs/workbench/buildfile.js index 4b83a39ab10203ccd155a168f78c35e75a6547a6..1dfb66c06b41eab6bdd77db66f000f3a38e56d14 100644 --- a/src/vs/workbench/buildfile.js +++ b/src/vs/workbench/buildfile.js @@ -27,8 +27,6 @@ exports.collectModules = function () { createModuleDescription('vs/workbench/services/files/node/watcher/nsfw/watcherApp', []), createModuleDescription('vs/workbench/node/extensionHostProcess', []), - - createModuleDescription('vs/workbench/parts/terminal/node/terminalProcess', []) ]; return modules; diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 81613ecef7bc337a230f7e14b312e4476d30c6c4..ffcd27e8f467280f33f903ba1ecc74c9d77b63c1 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -596,9 +596,9 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { 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; + onInput: Event; + onResize: Event<{ cols: number, rows: number }>; + onShutdown: Event; } export interface ITerminalProcessExtHostRequest { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts index 64a26d1017cd601c6d52edfd1662b591560965aa..c4d62a87eadf04e902f835c04703c140bbfea609 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcessManager.ts @@ -3,21 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -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'; import { IDisposable } from 'vs/base/common/lifecycle'; 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 { ITerminalChildProcess } 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'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -50,13 +49,13 @@ 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, + private readonly _terminalId: number, + private readonly _configHelper: ITerminalConfigHelper, @IHistoryService private readonly _historyService: IHistoryService, - @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILogService private _logService: ILogService + @ILogService private readonly _logService: ILogService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService ) { this.ptyProcessReady = new TPromise(c => { this.onProcessReady(() => { @@ -68,13 +67,11 @@ export class TerminalProcessManager implements ITerminalProcessManager { public dispose(): void { if (this._process) { - if (this._process.connected) { - // 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' }); - } + // 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.shutdown(); this._process = null; } this._disposables.forEach(d => d.dispose()); @@ -94,7 +91,6 @@ export class TerminalProcessManager implements ITerminalProcessManager { 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); } @@ -109,23 +105,41 @@ export class TerminalProcessManager implements ITerminalProcessManager { 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); + // Merge process env with the env from config and from shellLaunchConfig + const env = { ...process.env }; + terminalEnvironment.mergeEnvironments(env, envFromConfig); + terminalEnvironment.mergeEnvironments(env, shellLaunchConfig.env); - // 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); + // Sanitize the environment, removing any undesirable VS Code and Electron environment + // variables + terminalEnvironment.sanitizeEnvironment(env); - this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options); + // Adding other env keys necessary to create the process + const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); + + this._logService.debug(`Terminal process launching`, shellLaunchConfig, this.initialCwd, cols, rows, env); + this._process = new TerminalProcess(shellLaunchConfig, this.initialCwd, cols, rows, env); } this.processState = ProcessState.LAUNCHING; - this._process.on('message', message => this._onMessage(message)); - this._process.on('exit', exitCode => this._onExit(exitCode)); + this._process.onProcessData(data => { + this._onProcessData.fire(data); + }); + + this._process.onProcessIdReady(pid => { + this.shellProcessId = pid; + this._onProcessReady.fire(); + + // Send any queued data that's waiting + if (this._preLaunchInputQueue.length > 0) { + this._process.input(this._preLaunchInputQueue.join('')); + this._preLaunchInputQueue.length = 0; + } + }); + + this._process.onProcessTitleChanged(title => this._onProcessTitle.fire(title)); + this._process.onProcessExit(exitCode => this._onExit(exitCode)); setTimeout(() => { if (this.processState === ProcessState.LAUNCHING) { @@ -135,15 +149,17 @@ export class TerminalProcessManager implements ITerminalProcessManager { } public setDimensions(cols: number, rows: number): void { - if (this._process && this._process.connected) { - // The child process could aready be terminated - try { - 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') { - throw (error); - } + if (!this._process) { + return; + } + + // The child process could already be terminated + try { + this._process.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); } } } @@ -152,10 +168,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,31 +176,6 @@ 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 _onExit(exitCode: number): void { this._process = null; diff --git a/src/vs/workbench/parts/terminal/node/terminal.ts b/src/vs/workbench/parts/terminal/node/terminal.ts index 8cb9bc9dddd25bd8fbf0c03070f0d98ad75d72f8..8d787d74e93f7de4187c308e44b45375a825a492 100644 --- a/src/vs/workbench/parts/terminal/node/terminal.ts +++ b/src/vs/workbench/parts/terminal/node/terminal.ts @@ -7,30 +7,21 @@ import * as os from 'os'; 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; -} +import { Event } from 'vs/base/common/event'; /** * 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; + onProcessData: Event; + onProcessExit: Event; + onProcessIdReady: Event; + onProcessTitleChanged: Event; - on(event: 'exit', listener: (code: number) => void): this; - on(event: 'message', listener: (message: IMessageFromTerminalProcess) => void): this; + shutdown(): void; + input(data: string): void; + resize(cols: number, rows: number): void; } let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string = null; diff --git a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts index 8e2bca6daddce2d55fd46af3ff051eb8cd2edc0c..84c4aa560c417f66d4d7c19cd3188adb1977758b 100644 --- a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts @@ -8,7 +8,6 @@ import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; import pkg from 'vs/platform/node/package'; import Uri from 'vs/base/common/uri'; -import { IStringDictionary } from 'vs/base/common/collections'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -17,7 +16,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati * This module contains utility functions related to the environment, cwd and paths. */ -export function mergeEnvironments(parent: IStringDictionary, other: IStringDictionary) { +export function mergeEnvironments(parent: platform.IProcessEnvironment, other: platform.IProcessEnvironment): void { if (!other) { return; } @@ -44,7 +43,7 @@ export function mergeEnvironments(parent: IStringDictionary, other: IStr } } -function _mergeEnvironmentValue(env: IStringDictionary, key: string, value: string | null) { +function _mergeEnvironmentValue(env: platform.IProcessEnvironment, key: string, value: string | null): void { if (typeof value === 'string') { env[key] = value; } else { @@ -52,34 +51,44 @@ function _mergeEnvironmentValue(env: IStringDictionary, key: string, val } } -export function createTerminalEnv(parentEnv: IStringDictionary, shell: IShellLaunchConfig, cwd: string, locale: string, cols?: number, rows?: number): IStringDictionary { - const env = { ...parentEnv }; - if (shell.env) { - mergeEnvironments(env, shell.env); - } +export function sanitizeEnvironment(env: platform.IProcessEnvironment): void { + // Remove keys based on strings + const keysToRemove = [ + 'ELECTRON_ENABLE_STACK_DUMPING', + 'ELECTRON_ENABLE_LOGGING', + 'ELECTRON_NO_ASAR', + 'ELECTRON_NO_ATTACH_CONSOLE', + 'ELECTRON_RUN_AS_NODE', + 'GOOGLE_API_KEY', + 'VSCODE_CLI', + 'VSCODE_DEV', + 'VSCODE_IPC_HOOK', + 'VSCODE_LOGS', + 'VSCODE_NLS_CONFIG', + 'VSCODE_PORTABLE', + 'VSCODE_PID', + ]; + keysToRemove.forEach((key) => { + if (env[key]) { + delete env[key]; + } + }); + + // Remove keys based on regexp + Object.keys(env).forEach(key => { + if (key.search(/^VSCODE_NODE_CACHED_DATA_DIR_\d+$/) === 0) { + delete env[key]; + } + }); +} - env['PTYPID'] = process.pid.toString(); - env['PTYSHELL'] = shell.executable; +export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, locale: string | undefined): void { env['TERM_PROGRAM'] = 'vscode'; env['TERM_PROGRAM_VERSION'] = pkg.version; - if (shell.args) { - if (typeof shell.args === 'string') { - env[`PTYSHELLCMDLINE`] = shell.args; - } else { - shell.args.forEach((arg, i) => env[`PTYSHELLARG${i}`] = arg); - } - } - env['PTYCWD'] = cwd; env['LANG'] = _getLangEnvVariable(locale); - if (cols && rows) { - env['PTYCOLS'] = cols.toString(); - env['PTYROWS'] = rows.toString(); - } - env['AMD_ENTRYPOINT'] = 'vs/workbench/parts/terminal/node/terminalProcess'; - return env; } -export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary { +export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: platform.IProcessEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder): platform.IProcessEnvironment { Object.keys(env).forEach((key) => { if (typeof env[key] === 'string') { env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, env[key]); diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index 8987b3580495891ea5db90928bef4ac2f53d9573..5500c17791a0cf7c6a20bb60c8ab2825e3f78ce7 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -5,163 +5,121 @@ import * as os from 'os'; import * as path from 'path'; +import * as platform from 'vs/base/common/platform'; import * as pty from 'node-pty'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; + +export class TerminalProcess implements ITerminalChildProcess, IDisposable { + private _exitCode: number; + private _closeTimeout: number; + private _ptyProcess: pty.IPty; + private _currentTitle: string = ''; + + private readonly _onProcessData: Emitter = new Emitter(); + public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessExit: Emitter = new Emitter(); + public get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); + public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onProcessTitleChanged: Emitter = new Emitter(); + public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } + + constructor( + shellLaunchConfig: IShellLaunchConfig, + cwd: string, + cols: number, + rows: number, + env: platform.IProcessEnvironment + ) { + let shellName: string; + if (os.platform() === 'win32') { + shellName = path.basename(shellLaunchConfig.executable); + } 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'; + } -// 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); - -let closeTimeout: number; -let exitCode: number; - -// 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); + const options: pty.IPtyForkOptions = { + name: shellName, + cwd, + env, + cols, + rows + }; + + this._ptyProcess = pty.spawn(shellLaunchConfig.executable, shellLaunchConfig.args, options); + this._ptyProcess.on('data', (data) => { + this._onProcessData.fire(data); + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + this._queueProcessExit(); + } + }); + this._ptyProcess.on('exit', (code) => { + this._exitCode = code; + this._queueProcessExit(); + }); + + // TODO: We should no longer need to delay this since pty.spawn is sync + setTimeout(() => { + this._sendProcessId(); + }, 500); + this._setupTitlePolling(); } - 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(); + public dispose(): void { + this._onProcessData.dispose(); + this._onProcessExit.dispose(); + this._onProcessIdReady.dispose(); + this._onProcessTitleChanged.dispose(); } -}); - -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(); + private _setupTitlePolling() { + this._sendProcessTitle(); + setInterval(() => { + if (this._currentTitle !== this._ptyProcess.process) { + this._sendProcessTitle(); + } + }, 200); } -}); -sendProcessId(); -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++; + // 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); + } + this._closeTimeout = setTimeout(() => { + this._ptyProcess.kill(); + this._onProcessExit.fire(this._exitCode); + this.dispose(); + }, 250); } - 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++; + private _sendProcessId() { + this._onProcessIdReady.fire(this._ptyProcess.pid); } -} -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); -} + private _sendProcessTitle(): void { + this._currentTitle = this._ptyProcess.process; + this._onProcessTitleChanged.fire(this._currentTitle); + } -function sendProcessId() { - process.send({ - type: 'pid', - content: ptyProcess.pid - }); -} + public shutdown(): void { + this._queueProcessExit(); + } -function setupTitlePolling() { - sendProcessTitle(); - setInterval(function () { - if (currentTitle !== ptyProcess.process) { - sendProcessTitle(); - } - }, 200); -} + public input(data: string): void { + this._ptyProcess.write(data); + } -function sendProcessTitle() { - process.send({ - type: 'title', - content: ptyProcess.process - }); - currentTitle = ptyProcess.process; + 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)); + } } diff --git a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts index 9c842fbbfa8cd683a8115a5c32af2bdaea08858a..52bc33c6fa5b8ba2292fc9db857eac23313f73fc 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcessExtHostProxy.ts @@ -3,17 +3,30 @@ * 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 { ITerminalChildProcess } from 'vs/workbench/parts/terminal/node/terminal'; +import { Event, Emitter } from 'vs/base/common/event'; 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; +import { IDisposable } from 'vs/base/common/lifecycle'; +export class TerminalProcessExtHostProxy implements ITerminalChildProcess, ITerminalProcessExtHostProxy { private _disposables: IDisposable[] = []; + private readonly _onProcessData: Emitter = new Emitter(); + public get onProcessData(): Event { return this._onProcessData.event; } + private readonly _onProcessExit: Emitter = new Emitter(); + public get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessIdReady: Emitter = new Emitter(); + public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } + private readonly _onProcessTitleChanged: Emitter = new Emitter(); + public get onProcessTitleChanged(): Event { return this._onProcessTitleChanged.event; } + + private readonly _onInput: Emitter = new Emitter(); + public get onInput(): Event { return this._onInput.event; } + private readonly _onResize: Emitter<{ cols: number, rows: number }> = new Emitter<{ cols: number, rows: number }>(); + public get onResize(): Event<{ cols: number, rows: number }> { return this._onResize.event; } + private readonly _onShutdown: Emitter = new Emitter(); + public get onShutdown(): Event { return this._onShutdown.event; } + constructor( public terminalId: number, shellLaunchConfig: IShellLaunchConfig, @@ -21,8 +34,6 @@ export class TerminalProcessExtHostProxy extends EventEmitter implements ITermin rows: number, @ITerminalService private _terminalService: ITerminalService ) { - super(); - // TODO: Return TPromise indicating success? Teardown if failure? this._terminalService.requestExtHostProcess(this, shellLaunchConfig, cols, rows); } @@ -33,46 +44,30 @@ export class TerminalProcessExtHostProxy extends EventEmitter implements ITermin } public emitData(data: string): void { - this.emit('message', { type: 'data', content: data } as IMessageFromTerminalProcess); + this._onProcessData.fire(data); } public emitTitle(title: string): void { - this.emit('message', { type: 'title', content: title } as IMessageFromTerminalProcess); + this._onProcessTitleChanged.fire(title); } public emitPid(pid: number): void { - this.emit('message', { type: 'pid', content: pid } as IMessageFromTerminalProcess); + this._onProcessIdReady.fire(pid); } public emitExit(exitCode: number): void { - this.emit('exit', exitCode); + this._onProcessExit.fire(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 shutdown(): void { + this._onShutdown.fire(); } - 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 input(data: string): void { + this._onInput.fire(data); } - public onShutdown(listener: () => void): void { - const outerListener = () => listener(); - this.on('shutdown', outerListener); - this._disposables.push(toDisposable(() => this.removeListener('shutdown', outerListener))); + public resize(cols: number, rows: number): void { + this._onResize.fire({ cols, rows }); } } \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts index 5fae93b3d0183e96a454b8362002d10a7883b246..1fad535bd47e0c7e0bd7c26ee72570bad3a55e6b 100644 --- a/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/workbench/parts/terminal/test/node/terminalEnvironment.test.ts @@ -9,45 +9,47 @@ 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 { IStringDictionary } from 'vs/base/common/collections'; -import { IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal'; suite('Workbench - TerminalEnvironment', () => { - test('createTerminalEnv', function () { - const shell1 = { - executable: '/bin/foosh', - args: ['-bar', 'baz'] - }; - const parentEnv1: IStringDictionary = { - ok: true - } as any; - const env1 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/foo', 'en-au'); - assert.ok(env1['ok'], 'Parent environment is copied'); - assert.deepStrictEqual(parentEnv1, { ok: true }, 'Parent environment is unchanged'); - assert.equal(env1['PTYPID'], process.pid.toString(), 'PTYPID is equal to the current PID'); - assert.equal(env1['PTYSHELL'], '/bin/foosh', 'PTYSHELL is equal to the provided shell'); - assert.equal(env1['PTYSHELLARG0'], '-bar', 'PTYSHELLARG0 is equal to the first shell argument'); - assert.equal(env1['PTYSHELLARG1'], 'baz', 'PTYSHELLARG1 is equal to the first shell argument'); - assert.ok(!('PTYSHELLARG2' in env1), 'PTYSHELLARG2 is unset'); - assert.equal(env1['PTYCWD'], '/foo', 'PTYCWD is equal to requested cwd'); - assert.equal(env1['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); - - const shell2: IShellLaunchConfig = { - executable: '/bin/foosh', - args: [] - }; - const parentEnv2: IStringDictionary = { - LANG: 'en_US.UTF-8' - }; - const env2 = terminalEnvironment.createTerminalEnv(parentEnv2, shell2, '/foo', 'en-au'); - assert.ok(!('PTYSHELLARG0' in env2), 'PTYSHELLARG0 is unset'); - assert.equal(env2['PTYCWD'], '/foo', 'PTYCWD is equal to /foo'); - assert.equal(env2['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); - - const env3 = terminalEnvironment.createTerminalEnv(parentEnv1, shell1, '/', null); - assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 + test('addTerminalEnvironmentKeys', () => { + const env = { FOO: 'bar' }; + const locale = 'en-au'; + terminalEnvironment.addTerminalEnvironmentKeys(env, locale); + assert.equal(env['TERM_PROGRAM'], 'vscode'); + assert.equal(env['TERM_PROGRAM_VERSION'].search(/^\d+\.\d+\.\d+$/), 0); + assert.equal(env['LANG'], 'en_AU.UTF-8', 'LANG is equal to the requested locale with UTF-8'); + + const env2 = { FOO: 'bar' }; + terminalEnvironment.addTerminalEnvironmentKeys(env2, null); + assert.equal(env2['LANG'], 'en_US.UTF-8', 'LANG is equal to en_US.UTF-8 as fallback.'); // More info on issue #14586 + + const env3 = { LANG: 'en_US.UTF-8' }; + terminalEnvironment.addTerminalEnvironmentKeys(env3, null); + assert.equal(env3['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); + }); - const env4 = terminalEnvironment.createTerminalEnv(parentEnv2, shell1, '/', null); - assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG'); + test('sanitizeEnvironment', () => { + let env = { + FOO: 'bar', + ELECTRON_ENABLE_STACK_DUMPING: 'x', + ELECTRON_ENABLE_LOGGING: 'x', + ELECTRON_NO_ASAR: 'x', + ELECTRON_NO_ATTACH_CONSOLE: 'x', + ELECTRON_RUN_AS_NODE: 'x', + GOOGLE_API_KEY: 'x', + VSCODE_CLI: 'x', + VSCODE_DEV: 'x', + VSCODE_IPC_HOOK: 'x', + VSCODE_LOGS: 'x', + VSCODE_NLS_CONFIG: 'x', + VSCODE_PORTABLE: 'x', + VSCODE_PID: 'x', + VSCODE_NODE_CACHED_DATA_DIR_12345: 'x' + }; + terminalEnvironment.sanitizeEnvironment(env); + assert.equal(env['FOO'], 'bar'); + assert.equal(Object.keys(env).length, 1); }); suite('mergeEnvironments', () => {