diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 3d03cfbba78..d3ef8db64d2 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -361,5 +361,5 @@ export interface ITerminalInstance { /** * Sets the title of the terminal instance. */ - setTitle(title: string): void; + setTitle(title: string, eventFromProcess: boolean): void; } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 3b5b38af6be..4d3632ba2d0 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -622,7 +622,7 @@ export class RenameTerminalAction extends Action { prompt: nls.localize('workbench.action.terminal.rename.prompt', "Enter terminal name"), }).then(name => { if (name) { - terminalInstance.setTitle(name); + terminalInstance.setTitle(name, false); } }); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 057b2dc152d..53755c84f48 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -12,6 +12,7 @@ import * as platform from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; import Event, { Emitter } from 'vs/base/common/event'; import Uri from 'vs/base/common/uri'; +import { WindowsShellHelper } from 'vs/workbench/parts/terminal/electron-browser/windowsShellHelper'; import XTermTerminal = require('xterm'); import { Dimension } from 'vs/base/browser/builder'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -84,6 +85,7 @@ export class TerminalInstance implements ITerminalInstance { private _messageTitleListener: (message: { type: string, content: string }) => void; private _preLaunchInputQueue: string; private _initialCwd: string; + private _windowsShellHelper: WindowsShellHelper; private _widgetManager: TerminalWidgetManager; private _linkHandler: TerminalLinkHandler; @@ -138,6 +140,12 @@ export class TerminalInstance implements ITerminalInstance { this._createProcess(this._shellLaunchConfig); this._createXterm(); + if (platform.isWindows) { + this._processReady.then(() => { + this._windowsShellHelper = new WindowsShellHelper(this._processId, this._shellLaunchConfig.executable); + }); + } + // Only attach xterm.js to the DOM if the terminal panel has been opened before. if (_container) { this.attachToElement(_container); @@ -264,6 +272,13 @@ export class TerminalInstance implements ITerminalInstance { if (TabFocus.getTabFocusMode() && event.keyCode === 9) { return false; } + + // Windows does not get a process title event from terminalProcess so we check the name on enter + // messageTitleListener is falsy when the API/user renames the terminal so we don't override it + if (platform.isWindows && event.keyCode === 13 /* ENTER */ && this._messageTitleListener) { + this._windowsShellHelper.getShellName().then(title => this.setTitle(title, true)); + } + return undefined; }); this._instanceDisposables.push(dom.addDisposableListener(this._xterm.element, 'mouseup', (event: KeyboardEvent) => { @@ -519,17 +534,17 @@ export class TerminalInstance implements ITerminalInstance { const platformKey = platform.isWindows ? 'windows' : platform.isMacintosh ? 'osx' : 'linux'; const envFromConfig = { ...process.env, ...this._configHelper.config.env[platformKey] }; const env = TerminalInstance.createTerminalEnv(envFromConfig, shell, this._initialCwd, locale, this._cols, this._rows); - this._title = shell.name || ''; this._process = cp.fork(Uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=terminal'], { env, cwd: Uri.parse(path.dirname(require.toUrl('../node/terminalProcess'))).fsPath }); - if (!shell.name) { + if (shell.name) { + this.setTitle(shell.name, false); + } else { // Only listen for process title changes when a name is not provided this._messageTitleListener = (message) => { if (message.type === 'title') { - this._title = message.content ? message.content : ''; - this._onTitleChanged.fire(this._title); + this.setTitle(message.content ? message.content : '', true); } }; this._process.on('message', this._messageTitleListener); @@ -652,7 +667,7 @@ export class TerminalInstance implements ITerminalInstance { const oldTitle = this._title; this._createProcess(shell); if (oldTitle !== this._title) { - this._onTitleChanged.fire(this._title); + this.setTitle(this._title, true); } this._process.on('message', (message) => this._sendPtyDataToXterm(message)); @@ -829,19 +844,25 @@ export class TerminalInstance implements ITerminalInstance { this._terminalProcessFactory = factory; } - public setTitle(title: string): void { + public setTitle(title: string, eventFromProcess: boolean): void { + if (eventFromProcess) { + if (platform.isWindows) { + // Remove the .exe extension + title = path.basename(title.split('.exe')[0]); + } + } else { + // If the title has not been set by the API or the rename command, unregister the handler that + // automatically updates the terminal name + if (this._process && this._messageTitleListener) { + this._process.removeListener('message', this._messageTitleListener); + this._messageTitleListener = null; + } + } const didTitleChange = title !== this._title; this._title = title; if (didTitleChange) { this._onTitleChanged.fire(title); } - - // If the title was not set by the API, unregister the handler that - // automatically updates the terminal name - if (this._process && this._messageTitleListener) { - this._process.removeListener('message', this._messageTitleListener); - this._messageTitleListener = null; - } } } @@ -866,4 +887,4 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { if (scrollbarSliderActiveBackgroundColor) { collector.addRule(`.monaco-workbench .panel.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:active { background-color: ${scrollbarSliderActiveBackgroundColor}; }`); } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index 843d71e1ffc..9c5b6f5d900 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +'use strict'; + import * as dom from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; diff --git a/src/vs/workbench/parts/terminal/electron-browser/windowsShellHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/windowsShellHelper.ts new file mode 100644 index 00000000000..ab5f254324d --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/windowsShellHelper.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * 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 path from 'path'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Emitter, debounceEvent } from 'vs/base/common/event'; + +const SHELL_EXECUTABLES = ['cmd.exe', 'powershell.exe', 'bash.exe']; + +export class WindowsShellHelper { + private _childProcessIdStack: number[]; + private _onCheckWindowsShell: Emitter; + private _rootShellExecutable: string; + private _rootProcessId: number; + + public constructor(rootProcessId: number, rootShellExecutable: string) { + this._childProcessIdStack = []; + this._rootShellExecutable = rootShellExecutable; + this._rootProcessId = rootProcessId; + + if (!platform.isWindows) { + throw new Error(`WindowsShellHelper cannot be instantiated on ${platform.platform}`); + } + + this._onCheckWindowsShell = new Emitter(); + debounceEvent(this._onCheckWindowsShell.event, (l, e) => e, 100, true)(() => { + this.getShellName(); + }); + } + + private getChildProcessDetails(pid: number): TPromise<{ executable: string, pid: number }[]> { + return new TPromise((resolve, reject) => { + cp.execFile('wmic.exe', ['process', 'where', `parentProcessId=${pid}`, 'get', 'ExecutablePath,ProcessId'], (err, stdout, stderr) => { + if (err) { + reject(err); + } else if (stderr.length > 0) { + resolve([]); // No processes found + } else { + const childProcessLines = stdout.split('\n').slice(1).filter(str => !/^\s*$/.test(str)); + const childProcessDetails = childProcessLines.map(str => { + const s = str.split(' '); + return { executable: s[0], pid: Number(s[1]) }; + }); + resolve(childProcessDetails); + } + }); + }); + } + + private refreshShellProcessTree(pid: number, parent: string): TPromise { + return this.getChildProcessDetails(pid).then(result => { + // When we didn't find any child processes of the process + if (result.length === 0) { + // Case where we found a child process already and are checking further down the pid tree + // We have reached the end here so we know that parent is the deepest first child of the tree + if (parent) { + return TPromise.as(parent); + } + // Case where we haven't found a child and only the root shell is left + if (this._childProcessIdStack.length === 1) { + return TPromise.as(this._rootShellExecutable); + } + // Otherwise, we go up the tree to find the next valid deepest child of the root + this._childProcessIdStack.pop(); + return this.refreshShellProcessTree(this._childProcessIdStack[this._childProcessIdStack.length - 1], null); + } + // We only go one level deep when checking for children of processes other then shells + if (SHELL_EXECUTABLES.indexOf(path.basename(result[0].executable)) === -1) { + return TPromise.as(result[0].executable); + } + // Save the pid in the stack and keep looking for children of that child + this._childProcessIdStack.push(result[0].pid); + return this.refreshShellProcessTree(result[0].pid, result[0].executable); + }, error => { return error; }); + } + + /** + * Returns the innermost shell executable running in the terminal + */ + public getShellName(): TPromise { + if (this._childProcessIdStack.length === 0) { + this._childProcessIdStack.push(this._rootProcessId); + } + return new TPromise((resolve) => { + this.refreshShellProcessTree(this._childProcessIdStack[this._childProcessIdStack.length - 1], null).then(result => { + resolve(result); + }, error => { return error; }); + }); + } +} \ No newline at end of file