Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move terminalProcess into the renderer process #54090

Merged
merged 15 commits into from
Jul 12, 2018
1 change: 0 additions & 1 deletion build/gulpfile.vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
60 changes: 22 additions & 38 deletions src/vs/workbench/api/node/extHostTerminalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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);
Expand All @@ -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, <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 });
} 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];
Expand Down
2 changes: 0 additions & 2 deletions src/vs/workbench/buildfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/parts/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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());
Expand All @@ -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);
}
Expand All @@ -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) {
Expand All @@ -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);
}
}
}
Expand All @@ -152,42 +168,14 @@ 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
this._preLaunchInputQueue.push(data);
}
}

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;

Expand Down
25 changes: 8 additions & 17 deletions src/vs/workbench/parts/terminal/node/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down