未验证 提交 4986a041 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #54090 from Microsoft/tyriar/terminalProcess

Move terminalProcess into the renderer process
......@@ -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',
......
......@@ -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));
}
......
......@@ -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,41 +380,33 @@ 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, <number>message.content); break;
case 'title': this._proxy.$sendProcessTitle(id, <string>message.content); break;
case 'data': this._proxy.$sendProcessData(id, <string>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 });
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') {
......@@ -425,19 +414,14 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
}
}
}
}
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];
......
......@@ -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;
......
......@@ -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<string>;
onResize: Event<{ cols: number, rows: number }>;
onShutdown: Event<void>;
}
export interface ITerminalProcessExtHostRequest {
......
......@@ -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<number> { 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<void>(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' });
}
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,10 +149,13 @@ 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
if (!this._process) {
return;
}
// The child process could already be terminated
try {
this._process.send({ event: 'resize', cols, rows });
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') {
......@@ -146,16 +163,12 @@ export class TerminalProcessManager implements ITerminalProcessManager {
}
}
}
}
public write(data: string): void {
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(<string>message.content);
break;
case 'pid':
this.shellProcessId = <number>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(<string>message.content);
break;
}
}
private _onExit(exitCode: number): void {
this._process = null;
......
......@@ -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<string>;
onProcessExit: Event<number>;
onProcessIdReady: Event<number>;
onProcessTitleChanged: Event<string>;
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;
......
......@@ -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<string>, other: IStringDictionary<string>) {
export function mergeEnvironments(parent: platform.IProcessEnvironment, other: platform.IProcessEnvironment): void {
if (!other) {
return;
}
......@@ -44,7 +43,7 @@ export function mergeEnvironments(parent: IStringDictionary<string>, other: IStr
}
}
function _mergeEnvironmentValue(env: IStringDictionary<string>, 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<string>, key: string, val
}
}
export function createTerminalEnv(parentEnv: IStringDictionary<string>, shell: IShellLaunchConfig, cwd: string, locale: string, cols?: number, rows?: number): IStringDictionary<string> {
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];
}
});
env['PTYPID'] = process.pid.toString();
env['PTYSHELL'] = shell.executable;
// Remove keys based on regexp
Object.keys(env).forEach(key => {
if (key.search(/^VSCODE_NODE_CACHED_DATA_DIR_\d+$/) === 0) {
delete env[key];
}
});
}
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<string>, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary<string> {
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]);
......
......@@ -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';
// 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 {
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<string> = new Emitter<string>();
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessExit: Emitter<number> = new Emitter<number>();
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
private readonly _onProcessIdReady: Emitter<number> = new Emitter<number>();
public get onProcessIdReady(): Event<number> { return this._onProcessIdReady.event; }
private readonly _onProcessTitleChanged: Emitter<string> = new Emitter<string>();
public get onProcessTitleChanged(): Event<string> { 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';
}
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 = {
const options: pty.IPtyForkOptions = {
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);
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();
}
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();
}
});
ptyProcess.on('exit', function (code) {
exitCode = code;
queueProcessExit();
});
this._ptyProcess.on('exit', (code) => {
this._exitCode = code;
this._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();
// TODO: We should no longer need to delay this since pty.spawn is sync
setTimeout(() => {
this._sendProcessId();
}, 500);
this._setupTitlePolling();
}
});
sendProcessId();
setupTitlePolling();
public dispose(): void {
this._onProcessData.dispose();
this._onProcessExit.dispose();
this._onProcessIdReady.dispose();
this._onProcessTitleChanged.dispose();
}
function getArgs(): string | string[] {
if (process.env['PTYSHELLCMDLINE']) {
return process.env['PTYSHELLCMDLINE'];
private _setupTitlePolling() {
this._sendProcessTitle();
setInterval(() => {
if (this._currentTitle !== this._ptyProcess.process) {
this._sendProcessTitle();
}
const args = [];
let i = 0;
while (process.env['PTYSHELLARG' + i]) {
args.push(process.env['PTYSHELLARG' + i]);
i++;
}, 200);
}
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];
// 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);
}
});
let i = 0;
while (process.env['PTYSHELLARG' + i]) {
delete process.env['PTYSHELLARG' + i];
i++;
this._closeTimeout = setTimeout(() => {
this._ptyProcess.kill();
this._onProcessExit.fire(this._exitCode);
this.dispose();
}, 250);
}
}
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();
private _sendProcessId() {
this._onProcessIdReady.fire(this._ptyProcess.pid);
}
}, 5000);
}
function sendProcessId() {
process.send({
type: 'pid',
content: ptyProcess.pid
});
}
private _sendProcessTitle(): void {
this._currentTitle = this._ptyProcess.process;
this._onProcessTitleChanged.fire(this._currentTitle);
}
function setupTitlePolling() {
sendProcessTitle();
setInterval(function () {
if (currentTitle !== ptyProcess.process) {
sendProcessTitle();
public shutdown(): void {
this._queueProcessExit();
}
}, 200);
}
function sendProcessTitle() {
process.send({
type: 'title',
content: ptyProcess.process
});
currentTitle = ptyProcess.process;
public input(data: string): void {
this._ptyProcess.write(data);
}
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));
}
}
......@@ -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<string> = new Emitter<string>();
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessExit: Emitter<number> = new Emitter<number>();
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
private readonly _onProcessIdReady: Emitter<number> = new Emitter<number>();
public get onProcessIdReady(): Event<number> { return this._onProcessIdReady.event; }
private readonly _onProcessTitleChanged: Emitter<string> = new Emitter<string>();
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
private readonly _onInput: Emitter<string> = new Emitter<string>();
public get onInput(): Event<string> { 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<void> = new Emitter<void>();
public get onShutdown(): Event<void> { 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<boolean> 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
......@@ -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']
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');
});
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'
};
const parentEnv1: IStringDictionary<string> = {
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<string> = {
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
const env4 = terminalEnvironment.createTerminalEnv(parentEnv2, shell1, '/', null);
assert.equal(env4['LANG'], 'en_US.UTF-8', 'LANG is equal to the parent environment\'s LANG');
terminalEnvironment.sanitizeEnvironment(env);
assert.equal(env['FOO'], 'bar');
assert.equal(Object.keys(env).length, 1);
});
suite('mergeEnvironments', () => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册