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

Merge pull request #48226 from Microsoft/tyriar/terminal_process

Refactor terminal processes
......@@ -5,31 +5,32 @@
'use strict';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ITerminalService, ITerminalInstance, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal';
import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal';
import { TPromise } from 'vs/base/common/winjs.base';
import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext } from '../node/extHost.protocol';
import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, ShellLaunchConfigDto } from '../node/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {
private _proxy: ExtHostTerminalServiceShape;
private _toDispose: IDisposable[];
private _toDispose: IDisposable[] = [];
private _terminalProcesses: { [id: number]: ITerminalProcessExtHostProxy } = {};
constructor(
extHostContext: IExtHostContext,
@ITerminalService private terminalService: ITerminalService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService);
this._toDispose = [];
this._toDispose.push(terminalService.onInstanceCreated((terminalInstance) => {
// Delay this message so the TerminalInstance constructor has a chance to finish and
// return the ID normally to the extension host. The ID that is passed here will be used
// to register non-extension API terminals in the extension host.
setTimeout(() => this._onTerminalOpened(terminalInstance), 100);
}));
this._toDispose.push(terminalService.onInstanceDisposed((terminalInstance) => this._onTerminalDisposed(terminalInstance)));
this._toDispose.push(terminalService.onInstanceProcessIdReady((terminalInstance) => this._onTerminalProcessIdReady(terminalInstance)));
this._toDispose.push(terminalService.onInstanceDisposed(terminalInstance => this._onTerminalDisposed(terminalInstance)));
this._toDispose.push(terminalService.onInstanceProcessIdReady(terminalInstance => this._onTerminalProcessIdReady(terminalInstance)));
this._toDispose.push(terminalService.onInstanceRequestExtHostProcess(request => this._onTerminalRequestExtHostProcess(request)));
// Set initial ext host state
this.terminalService.terminalInstances.forEach(t => {
......@@ -97,4 +98,36 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void {
this._proxy.$acceptTerminalProcessId(terminalInstance.id, terminalInstance.processId);
}
private _onTerminalRequestExtHostProcess(request: ITerminalProcessExtHostRequest): void {
this._terminalProcesses[request.proxy.terminalId] = request.proxy;
const shellLaunchConfigDto: ShellLaunchConfigDto = {
name: request.shellLaunchConfig.name,
executable: request.shellLaunchConfig.executable,
args: request.shellLaunchConfig.args,
cwd: request.shellLaunchConfig.cwd,
env: request.shellLaunchConfig.env
};
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.onShutdown(() => this._proxy.$acceptProcessShutdown(request.proxy.terminalId));
}
public $sendProcessTitle(terminalId: number, title: string): void {
this._terminalProcesses[terminalId].emitTitle(title);
}
public $sendProcessData(terminalId: number, data: string): void {
this._terminalProcesses[terminalId].emitData(data);
}
public $sendProcessPid(terminalId: number, pid: number): void {
this._terminalProcesses[terminalId].emitPid(pid);
}
public $sendProcessExit(terminalId: number, exitCode: number): void {
this._terminalProcesses[terminalId].emitExit(exitCode);
delete this._terminalProcesses[terminalId];
}
}
......@@ -114,7 +114,7 @@ export function createApiFactory(
const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures));
const extHostFileSystemEvent = rpcProtocol.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService());
const extHostQuickOpen = rpcProtocol.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(rpcProtocol, extHostWorkspace, extHostCommands));
const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol));
const extHostTerminalService = rpcProtocol.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(rpcProtocol, extHostConfiguration, extHostLogService));
const extHostSCM = rpcProtocol.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(rpcProtocol, extHostCommands, extHostLogService));
const extHostSearch = rpcProtocol.set(ExtHostContext.ExtHostSearch, new ExtHostSearch(rpcProtocol));
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace));
......
......@@ -319,6 +319,11 @@ export interface MainThreadTerminalServiceShape extends IDisposable {
$hide(terminalId: number): void;
$sendText(terminalId: number, text: string, addNewLine: boolean): void;
$show(terminalId: number, preserveFocus: boolean): void;
$sendProcessTitle(terminalId: number, title: string): void;
$sendProcessData(terminalId: number, data: string): void;
$sendProcessPid(terminalId: number, pid: number): void;
$sendProcessExit(terminalId: number, exitCode: number): void;
}
export interface MyQuickPickItems extends IPickOpenEntry {
......@@ -729,10 +734,22 @@ export interface ExtHostQuickOpenShape {
$validateInput(input: string): TPromise<string>;
}
export interface ShellLaunchConfigDto {
name?: string;
executable?: string;
args?: string[] | string;
cwd?: string;
env?: { [key: string]: string };
}
export interface ExtHostTerminalServiceShape {
$acceptTerminalClosed(id: number): void;
$acceptTerminalOpened(id: number, name: string): void;
$acceptTerminalProcessId(id: number, processId: number): void;
$createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void;
$acceptProcessInput(id: number, data: string): void;
$acceptProcessResize(id: number, cols: number, rows: number): void;
$acceptProcessShutdown(id: number): void;
}
export interface ExtHostSCMShape {
......
......@@ -5,11 +5,18 @@
'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 } from './extHost.protocol';
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';
export class ExtHostTerminal implements vscode.Terminal {
private _name: string;
private _id: number;
private _proxy: MainThreadTerminalServiceShape;
......@@ -81,8 +88,11 @@ export class ExtHostTerminal implements vscode.Terminal {
}
public _setProcessId(processId: number): void {
this._pidPromiseComplete(processId);
this._pidPromiseComplete = null;
// The event may fire 2 times when the panel is restored
if (this._pidPromiseComplete) {
this._pidPromiseComplete(processId);
this._pidPromiseComplete = null;
}
}
private _queueApiRequest(callback: (...args: any[]) => void, args: any[]) {
......@@ -102,19 +112,22 @@ export class ExtHostTerminal implements vscode.Terminal {
}
export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
private readonly _onDidCloseTerminal: Emitter<vscode.Terminal>;
private readonly _onDidOpenTerminal: Emitter<vscode.Terminal>;
private _proxy: MainThreadTerminalServiceShape;
private _terminals: ExtHostTerminal[];
private _terminals: ExtHostTerminal[] = [];
private _terminalProcesses: { [id: number]: cp.ChildProcess } = {};
public get terminals(): ExtHostTerminal[] { return this._terminals; }
constructor(mainContext: IMainContext) {
constructor(
mainContext: IMainContext,
private _extHostConfiguration: ExtHostConfiguration,
private _logService: ILogService
) {
this._onDidCloseTerminal = new Emitter<vscode.Terminal>();
this._onDidOpenTerminal = new Emitter<vscode.Terminal>();
this._proxy = mainContext.getProxy(MainContext.MainThreadTerminalService);
this._terminals = [];
}
public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal {
......@@ -168,6 +181,99 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
}
}
public $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void {
// TODO: This function duplicates a lot of TerminalProcessManager.createProcess, ideally
// they would be merged into a single implementation.
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);
const platformKey = platform.isWindows ? 'windows' : platform.isMacintosh ? 'osx' : 'linux';
const shellConfigValue: string = terminalConfig.get(`shell.${platformKey}`);
const shellArgsConfigValue: string = terminalConfig.get(`shellArgs.${platformKey}`);
shellLaunchConfig.executable = shellConfigValue;
shellLaunchConfig.args = shellArgsConfigValue;
}
// TODO: Base the cwd on the last active workspace root
// const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file');
// this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper);
const initialCwd = os.homedir();
// TODO: Pull in and resolve config settings
// // Resolve env vars from config and shell
// const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri);
// const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
// const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot);
// const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot);
// shellLaunchConfig.env = envFromShell;
// Merge process env with the env from config
const parentEnv = { ...process.env };
// terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig);
// Continue env initialization, merging in the env from the launch
// config and adding keys that are needed to create the process
const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, initialCwd, locale, cols, rows);
let cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath;
const options = { env, cwd, execArgv: [] };
// 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));
}
public $acceptProcessInput(id: number, data: string): void {
if (this._terminalProcesses[id].connected) {
this._terminalProcesses[id].send({ event: '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);
}
}
}
}
public $acceptProcessShutdown(id: number): void {
if (this._terminalProcesses[id].connected) {
this._terminalProcesses[id].send({ event: 'shutdown' });
}
}
private _onProcessExit(id: number, exitCode: number): void {
// Remove listeners
const process = this._terminalProcesses[id];
process.removeAllListeners('message');
process.removeAllListeners('exit');
// Remove process reference
delete this._terminalProcesses[id];
// Send exit event to main side
this._proxy.$sendProcessExit(id, exitCode);
}
private _getTerminalById(id: number): ExtHostTerminal {
let index = this._getTerminalIndexById(id);
return index !== null ? this._terminals[index] : null;
......@@ -176,6 +282,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
private _getTerminalIndexById(id: number): number {
let index: number = null;
this._terminals.some((terminal, i) => {
// TODO: This shouldn't be cas
let thisId = (<any>terminal)._id;
if (thisId === id) {
index = i;
......@@ -188,7 +295,6 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape {
}
class ApiRequest {
private _callback: (...args: any[]) => void;
private _args: any[];
......
......@@ -103,26 +103,35 @@ export interface ITerminalFont {
}
export interface IShellLaunchConfig {
/** The name of the terminal, if this is not set the name of the process will be used. */
/**
* The name of the terminal, if this is not set the name of the process will be used.
*/
name?: string;
/** The shell executable (bash, cmd, etc.). */
/**
* The shell executable (bash, cmd, etc.).
*/
executable?: string;
/**
* The CLI arguments to use with executable, a string[] is in argv format and will be escaped,
* a string is in "CommandLine" pre-escaped format and will be used as is. The string option is
* only supported on Windows and will throw an exception if used on macOS or Linux.
*/
args?: string[] | string;
/**
* The current working directory of the terminal, this overrides the `terminal.integrated.cwd`
* settings key.
*/
cwd?: string;
/**
* A custom environment for the terminal, if this is not set the environment will be inherited
* from the VS Code process.
*/
env?: { [key: string]: string };
/**
* Whether to ignore a custom cwd from the `terminal.integrated.cwd` settings key (eg. if the
* shell is being launched by an extension).
......@@ -151,6 +160,7 @@ export interface ITerminalService {
onInstanceCreated: Event<ITerminalInstance>;
onInstanceDisposed: Event<ITerminalInstance>;
onInstanceProcessIdReady: Event<ITerminalInstance>;
onInstanceRequestExtHostProcess: Event<ITerminalProcessExtHostRequest>;
onInstancesChanged: Event<void>;
onInstanceTitleChanged: Event<string>;
terminalInstances: ITerminalInstance[];
......@@ -191,6 +201,8 @@ export interface ITerminalService {
setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
selectDefaultWindowsShell(): TPromise<string>;
setWorkspaceShellAllowed(isAllowed: boolean): void;
requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void;
}
export const enum Direction {
......@@ -226,9 +238,10 @@ export interface ITerminalInstance {
id: number;
/**
* The process ID of the shell process.
* The process ID of the shell process, this is undefined when there is no process associated
* with this terminal.
*/
processId: number;
processId: number | undefined;
/**
* An event that fires when the terminal instance's title changes.
......@@ -244,6 +257,8 @@ export interface ITerminalInstance {
onProcessIdReady: Event<ITerminalInstance>;
onRequestExtHostProcess: Event<ITerminalInstance>;
processReady: TPromise<void>;
/**
......@@ -466,11 +481,6 @@ export interface ITerminalCommandTracker {
selectToNextCommand(): void;
}
export interface ITerminalProcessMessage {
type: 'pid' | 'data' | 'title';
content: number | string;
}
export interface ITerminalProcessManager extends IDisposable {
readonly processState: ProcessState;
readonly ptyProcessReady: TPromise<void>;
......@@ -506,3 +516,24 @@ export enum ProcessState {
// was run.
KILLED_BY_PROCESS
}
export interface ITerminalProcessExtHostProxy extends IDisposable {
readonly terminalId: number;
emitData(data: string): void;
emitTitle(title: string): void;
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;
}
export interface ITerminalProcessExtHostRequest {
proxy: ITerminalProcessExtHostProxy;
shellLaunchConfig: IShellLaunchConfig;
cols: number;
rows: number;
}
\ No newline at end of file
......@@ -9,7 +9,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab } from 'vs/workbench/parts/terminal/common/terminal';
import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal';
import { TPromise } from 'vs/base/common/winjs.base';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
......@@ -22,28 +22,31 @@ export abstract class TerminalService implements ITerminalService {
protected _terminalFocusContextKey: IContextKey<boolean>;
protected _findWidgetVisible: IContextKey<boolean>;
protected _terminalContainer: HTMLElement;
protected _onInstancesChanged: Emitter<void>;
protected _onTabDisposed: Emitter<ITerminalTab>;
protected _onInstanceCreated: Emitter<ITerminalInstance>;
protected _onInstanceDisposed: Emitter<ITerminalInstance>;
protected _onInstanceProcessIdReady: Emitter<ITerminalInstance>;
protected _onInstanceTitleChanged: Emitter<string>;
protected _terminalTabs: ITerminalTab[];
protected abstract _terminalInstances: ITerminalInstance[];
private _activeTabIndex: number;
private readonly _onActiveTabChanged: Emitter<void>;
public get activeTabIndex(): number { return this._activeTabIndex; }
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; }
private readonly _onActiveTabChanged: Emitter<void> = new Emitter<void>();
public get onActiveTabChanged(): Event<void> { return this._onActiveTabChanged.event; }
public get onTabDisposed(): Event<ITerminalTab> { return this._onTabDisposed.event; }
protected readonly _onInstanceCreated: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
public get onInstanceCreated(): Event<ITerminalInstance> { return this._onInstanceCreated.event; }
protected readonly _onInstanceDisposed: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
public get onInstanceDisposed(): Event<ITerminalInstance> { return this._onInstanceDisposed.event; }
protected readonly _onInstanceProcessIdReady: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
public get onInstanceProcessIdReady(): Event<ITerminalInstance> { return this._onInstanceProcessIdReady.event; }
public get onInstanceTitleChanged(): Event<string> { return this._onInstanceTitleChanged.event; }
protected readonly _onInstanceRequestExtHostProcess: Emitter<ITerminalProcessExtHostRequest> = new Emitter<ITerminalProcessExtHostRequest>();
public get onInstanceRequestExtHostProcess(): Event<ITerminalProcessExtHostRequest> { return this._onInstanceRequestExtHostProcess.event; }
protected readonly _onInstancesChanged: Emitter<void> = new Emitter<void>();
public get onInstancesChanged(): Event<void> { return this._onInstancesChanged.event; }
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; }
protected readonly _onInstanceTitleChanged: Emitter<string> = new Emitter<string>();
public get onInstanceTitleChanged(): Event<string> { return this._onInstanceTitleChanged.event; }
protected readonly _onTabDisposed: Emitter<ITerminalTab> = new Emitter<ITerminalTab>();
public get onTabDisposed(): Event<ITerminalTab> { return this._onTabDisposed.event; }
public abstract get configHelper(): ITerminalConfigHelper;
......@@ -57,14 +60,6 @@ export abstract class TerminalService implements ITerminalService {
this._activeTabIndex = 0;
this._isShuttingDown = false;
this._onActiveTabChanged = new Emitter<void>();
this._onTabDisposed = new Emitter<ITerminalTab>();
this._onInstanceCreated = new Emitter<ITerminalInstance>();
this._onInstanceDisposed = new Emitter<ITerminalInstance>();
this._onInstanceProcessIdReady = new Emitter<ITerminalInstance>();
this._onInstanceTitleChanged = new Emitter<string>();
this._onInstancesChanged = new Emitter<void>();
lifecycleService.onWillShutdown(event => event.veto(this._onWillShutdown()));
lifecycleService.onShutdown(() => this._onShutdown());
this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService);
......@@ -80,6 +75,7 @@ export abstract class TerminalService implements ITerminalService {
public abstract getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance;
public abstract selectDefaultWindowsShell(): TPromise<string>;
public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
public abstract requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void;
private _restoreTabs(): void {
if (!this.configHelper.config.experimentalRestore) {
......
......@@ -14,7 +14,7 @@ import * as platform from 'vs/base/common/platform';
import * as terminalCommands from 'vs/workbench/parts/terminal/common/terminalCommands';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TerminalCursorStyle, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE } from 'vs/workbench/parts/terminal/common/terminal';
import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
......
......@@ -12,7 +12,7 @@ import { IWorkspaceConfigurationService } from 'vs/workbench/services/configurat
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ITerminalConfiguration, ITerminalConfigHelper, ITerminalFont, IShellLaunchConfig, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION } from 'vs/workbench/parts/terminal/common/terminal';
import Severity from 'vs/base/common/severity';
import { isFedora } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { isFedora } from 'vs/workbench/parts/terminal/node/terminal';
import { Terminal as XTermTerminal } from 'vscode-xterm';
import { INotificationService } from 'vs/platform/notification/common/notification';
......
......@@ -71,24 +71,27 @@ export class TerminalInstance implements ITerminalInstance {
public disableLayout: boolean;
public get id(): number { return this._id; }
// TODO: Ideally processId would be merged into processReady
public get processId(): number { return this._processManager.shellProcessId; }
public get processId(): number | undefined { return this._processManager ? this._processManager.shellProcessId : undefined; }
// TODO: How does this work with detached processes?
// TODO: Should this be an event as it can fire twice?
public get processReady(): TPromise<void> { return this._processManager.ptyProcessReady; }
public get processReady(): TPromise<void> { return this._processManager ? this._processManager.ptyProcessReady : TPromise.as(void 0); }
public get title(): string { return this._title; }
public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; }
public get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; }
public get shellLaunchConfig(): IShellLaunchConfig { return Object.freeze(this._shellLaunchConfig); }
public get commandTracker(): TerminalCommandTracker { return this._commandTracker; }
private readonly _onDisposed: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
private readonly _onFocused: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
private readonly _onProcessIdReady: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
private readonly _onTitleChanged: Emitter<string> = new Emitter<string>();
private readonly _onDisposed: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
public get onDisposed(): Event<ITerminalInstance> { return this._onDisposed.event; }
private readonly _onFocused: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
public get onFocused(): Event<ITerminalInstance> { return this._onFocused.event; }
private readonly _onProcessIdReady: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
public get onProcessIdReady(): Event<ITerminalInstance> { return this._onProcessIdReady.event; }
private readonly _onTitleChanged: Emitter<string> = new Emitter<string>();
public get onTitleChanged(): Event<string> { return this._onTitleChanged.event; }
private readonly _onRequestExtHostProcess: Emitter<ITerminalInstance> = new Emitter<ITerminalInstance>();
public get onRequestExtHostProcess(): Event<ITerminalInstance> { return this._onRequestExtHostProcess.event; }
public constructor(
private _terminalFocusContextKey: IContextKey<boolean>,
......@@ -267,8 +270,9 @@ export class TerminalInstance implements ITerminalInstance {
if (this._processManager) {
this._processManager.onProcessData(data => this._sendPtyDataToXterm(data));
this._xterm.on('data', data => this._processManager.write(data));
// TODO: How does the cwd work on detached processes?
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager.initialCwd);
}
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager.initialCwd);
this._commandTracker = new TerminalCommandTracker(this._xterm);
this._disposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme)));
}
......@@ -394,10 +398,13 @@ export class TerminalInstance implements ITerminalInstance {
}));
this._wrapperElement.appendChild(this._xtermElement);
this._widgetManager = new TerminalWidgetManager(this._wrapperElement);
this._linkHandler.setWidgetManager(this._widgetManager);
this._container.appendChild(this._wrapperElement);
if (this._processManager) {
this._widgetManager = new TerminalWidgetManager(this._wrapperElement);
this._linkHandler.setWidgetManager(this._widgetManager);
}
const computedStyle = window.getComputedStyle(this._container);
const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10);
const height = parseInt(computedStyle.getPropertyValue('height').replace('px', ''), 10);
......@@ -584,8 +591,7 @@ export class TerminalInstance implements ITerminalInstance {
}
protected _createProcess(): void {
// TODO: This should be injected in to the terminal instance (from service?)
this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._configHelper);
this._processManager = this._instantiationService.createInstance(TerminalProcessManager, this._id, this._configHelper);
this._processManager.onProcessReady(() => this._onProcessIdReady.fire(this));
this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode));
this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows);
......@@ -667,7 +673,11 @@ export class TerminalInstance implements ITerminalInstance {
return a;
}).join(' ');
}
this._notificationService.error(nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode));
if (this._shellLaunchConfig.executable) {
this._notificationService.error(nls.localize('terminal.integrated.launchFailed', 'The terminal process command \'{0}{1}\' failed to launch (exit code: {2})', this._shellLaunchConfig.executable, args, exitCode));
} else {
this._notificationService.error(nls.localize('terminal.integrated.launchFailedExtHost', 'The terminal process failed to launch (exit code: {0})', exitCode));
}
} else {
if (this._configHelper.config.showExitAlert) {
this._notificationService.error(exitCodeMessage);
......@@ -868,7 +878,9 @@ export class TerminalInstance implements ITerminalInstance {
}
}
this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(this._cols, this._rows));
if (this._processManager) {
this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(this._cols, this._rows));
}
}
public setTitle(title: string, eventFromProcess: boolean): void {
......
......@@ -4,18 +4,20 @@
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import * as path from 'path';
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, ITerminalProcessMessage, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal';
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 { TerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/node/terminalProcessExtHostProxy';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;
......@@ -34,7 +36,7 @@ export class TerminalProcessManager implements ITerminalProcessManager {
public shellProcessId: number;
public initialCwd: string;
private _process: cp.ChildProcess;
private _process: ITerminalChildProcess;
private _preLaunchInputQueue: string[] = [];
private _disposables: IDisposable[] = [];
......@@ -48,12 +50,20 @@ 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,
@IHistoryService private readonly _historyService: IHistoryService,
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ILogService private _logService: ILogService
) {
this.ptyProcessReady = new TPromise<void>(c => {
this.onProcessReady(() => {
this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`);
c(void 0);
});
});
}
public dispose(): void {
......@@ -80,39 +90,38 @@ export class TerminalProcessManager implements ITerminalProcessManager {
cols: number,
rows: number
): void {
this.ptyProcessReady = new TPromise<void>(c => {
this.onProcessReady(() => {
this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`);
c(void 0);
});
});
const extensionHostOwned = (<any>this._configHelper.config).extHostProcess;
if (extensionHostOwned) {
this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, cols, rows);
} else {
const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined;
if (!shellLaunchConfig.executable) {
this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig);
}
const locale = this._configHelper.config.setLocaleVariables ? platform.locale : undefined;
if (!shellLaunchConfig.executable) {
this._configHelper.mergeDefaultShellPathAndArgs(shellLaunchConfig);
}
const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file');
this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper);
const lastActiveWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot('file');
this.initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, lastActiveWorkspaceRootUri, this._configHelper);
// Resolve env vars from config and shell
const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri);
const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot);
const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot);
shellLaunchConfig.env = envFromShell;
// Merge process env with the env from config
const parentEnv = { ...process.env };
terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig);
// Continue env initialization, merging in the env from the launch
// config and adding keys that are needed to create the process
const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows);
const cwd = Uri.parse(path.dirname(require.toUrl('../node/terminalProcess'))).fsPath;
const options = { env, cwd };
this._logService.debug(`Terminal process launching`, options);
this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options);
// Resolve env vars from config and shell
const lastActiveWorkspaceRoot = this._workspaceContextService.getWorkspaceFolder(lastActiveWorkspaceRootUri);
const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
const envFromConfig = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...this._configHelper.config.env[platformKey] }, lastActiveWorkspaceRoot);
const envFromShell = terminalEnvironment.resolveConfigurationVariables(this._configurationResolverService, { ...shellLaunchConfig.env }, lastActiveWorkspaceRoot);
shellLaunchConfig.env = envFromShell;
// Merge process env with the env from config
const parentEnv = { ...process.env };
terminalEnvironment.mergeEnvironments(parentEnv, envFromConfig);
// Continue env initialization, merging in the env from the launch
// config and adding keys that are needed to create the process
const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, this.initialCwd, locale, cols, rows);
const cwd = Uri.parse(require.toUrl('../node')).fsPath;
const options = { env, cwd };
this._logService.debug(`Terminal process launching`, options);
this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], options);
}
this.processState = ProcessState.LAUNCHING;
this._process.on('message', message => this._onMessage(message));
......@@ -152,7 +161,7 @@ export class TerminalProcessManager implements ITerminalProcessManager {
}
}
private _onMessage(message: ITerminalProcessMessage): void {
private _onMessage(message: IMessageFromTerminalProcess): void {
this._logService.trace(`terminalProcessManager#_onMessage (shellProcessId: ${this.shellProcessId}`, message);
switch (message.type) {
case 'data':
......
......@@ -13,13 +13,13 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IQuickOpenService, IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen';
import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfigHelper, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal/common/terminal';
import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfigHelper, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, TERMINAL_PANEL_ID, ITerminalProcessExtHostProxy } from 'vs/workbench/parts/terminal/common/terminal';
import { TerminalService as AbstractTerminalService } from 'vs/workbench/parts/terminal/common/terminalService';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { TPromise } from 'vs/base/common/winjs.base';
import Severity from 'vs/base/common/severity';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { getTerminalDefaultShellWindows } from 'vs/workbench/parts/terminal/node/terminal';
import { TerminalPanel } from 'vs/workbench/parts/terminal/electron-browser/terminalPanel';
import { TerminalTab } from 'vs/workbench/parts/terminal/browser/terminalTab';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
......@@ -27,6 +27,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { ipcRenderer as ipc } from 'electron';
import { IOpenFileRequest } from 'vs/platform/windows/common/windows';
import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export class TerminalService extends AbstractTerminalService implements ITerminalService {
private _configHelper: TerminalConfigHelper;
......@@ -47,7 +48,8 @@ export class TerminalService extends AbstractTerminalService implements ITermina
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IQuickOpenService private readonly _quickOpenService: IQuickOpenService,
@INotificationService private readonly _notificationService: INotificationService,
@IDialogService private readonly _dialogService: IDialogService
@IDialogService private readonly _dialogService: IDialogService,
@IExtensionService private readonly _extensionService: IExtensionService
) {
super(contextKeyService, panelService, partService, lifecycleService, storageService);
......@@ -90,7 +92,17 @@ export class TerminalService extends AbstractTerminalService implements ITermina
}
public createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance {
return this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, undefined, shellLaunchConfig, true);
return this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig, true);
}
public requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number): void {
// Ensure extension host is ready before requesting a process
this._extensionService.whenInstalledExtensionsRegistered().then(() => {
// TODO: MainThreadTerminalService is not ready at this point, fix this
setTimeout(() => {
this._onInstanceRequestExtHostProcess.fire({ proxy, shellLaunchConfig, cols, rows });
}, 500);
});
}
public focusFindWidget(): TPromise<void> {
......
......@@ -8,6 +8,31 @@ 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;
}
/**
* 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;
on(event: 'exit', listener: (code: number) => void): this;
on(event: 'message', listener: (message: IMessageFromTerminalProcess) => void): this;
}
let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string = null;
export function getTerminalDefaultShellUnixLike(): string {
if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) {
......@@ -50,4 +75,4 @@ if (platform.isLinux) {
});
}
export let isFedora = false;
export let isFedora = false;
\ No newline at end of file
......@@ -79,7 +79,6 @@ export function createTerminalEnv(parentEnv: IStringDictionary<string>, shell: I
return env;
}
// TODO:should be protected/non-static
export function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: IStringDictionary<string>, lastActiveWorkspaceRoot: IWorkspaceFolder): IStringDictionary<string> {
Object.keys(env).forEach((key) => {
if (typeof env[key] === 'string') {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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 { 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;
private _disposables: IDisposable[] = [];
constructor(
public terminalId: number,
shellLaunchConfig: IShellLaunchConfig,
cols: number,
rows: number,
@ITerminalService private _terminalService: ITerminalService
) {
super();
// TODO: Return TPromise<boolean> indicating success? Teardown if failure?
this._terminalService.requestExtHostProcess(this, shellLaunchConfig, cols, rows);
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
public emitData(data: string): void {
this.emit('message', { type: 'data', content: data } as IMessageFromTerminalProcess);
}
public emitTitle(title: string): void {
this.emit('message', { type: 'title', content: title } as IMessageFromTerminalProcess);
}
public emitPid(pid: number): void {
this.emit('message', { type: 'pid', content: pid } as IMessageFromTerminalProcess);
}
public emitExit(exitCode: number): void {
this.emit('exit', 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 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 onShutdown(listener: () => void): void {
const outerListener = () => listener();
this.on('shutdown', outerListener);
this._disposables.push(toDisposable(() => this.removeListener('shutdown', outerListener)));
}
}
\ No newline at end of file
......@@ -6,7 +6,7 @@
import * as assert from 'assert';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
import { isFedora } from 'vs/workbench/parts/terminal/electron-browser/terminal';
import { isFedora } from 'vs/workbench/parts/terminal/node/terminal';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
suite('Workbench - TerminalConfigHelper', () => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册