diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 3df611b17b46949afe7a968a364fd03f5f6ff965..7f99d93ada3a880dd5d2856171d17dceadf5ef5d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -97,6 +97,27 @@ suite('window namespace tests', () => { const terminal = window.createTerminal('b'); }); + test('exitStatus.code should be set to undefined after a terminal is disposed', (done) => { + disposables.push(window.onDidOpenTerminal(term => { + try { + equal(term, terminal); + } catch (e) { + done(e); + } + disposables.push(window.onDidCloseTerminal(t => { + try { + deepEqual(t.exitStatus, { code: undefined }); + } catch (e) { + done(e); + return; + } + done(); + })); + terminal.dispose(); + })); + const terminal = window.createTerminal(); + }); + // test('onDidChangeActiveTerminal should fire when new terminals are created', (done) => { // const reg1 = window.onDidChangeActiveTerminal((active: Terminal | undefined) => { // equal(active, terminal); @@ -362,9 +383,97 @@ suite('window namespace tests', () => { const pty: Pseudoterminal = { onDidWrite: writeEmitter.event, onDidOverrideDimensions: overrideDimensionsEmitter.event, - open: () => { - overrideDimensionsEmitter.fire({ columns: 10, rows: 5 }); - }, + open: () => overrideDimensionsEmitter.fire({ columns: 10, rows: 5 }), + close: () => { } + }; + const terminal = window.createTerminal({ name: 'foo', pty }); + }); + + test('exitStatus.code should be set to the exit code (undefined)', (done) => { + disposables.push(window.onDidOpenTerminal(term => { + try { + equal(terminal, term); + equal(terminal.exitStatus, undefined); + } catch (e) { + done(e); + } + disposables.push(window.onDidCloseTerminal(t => { + try { + equal(terminal, t); + deepEqual(terminal.exitStatus, { code: undefined }); + } catch (e) { + done(e); + return; + } + done(); + })); + })); + const writeEmitter = new EventEmitter(); + const closeEmitter = new EventEmitter(); + const pty: Pseudoterminal = { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: () => closeEmitter.fire(), + close: () => { } + }; + const terminal = window.createTerminal({ name: 'foo', pty }); + }); + + test('exitStatus.code should be set to the exit code (zero)', (done) => { + disposables.push(window.onDidOpenTerminal(term => { + try { + equal(terminal, term); + equal(terminal.exitStatus, undefined); + } catch (e) { + done(e); + } + disposables.push(window.onDidCloseTerminal(t => { + try { + equal(terminal, t); + deepEqual(terminal.exitStatus, { code: 0 }); + } catch (e) { + done(e); + return; + } + done(); + })); + })); + const writeEmitter = new EventEmitter(); + const closeEmitter = new EventEmitter(); + const pty: Pseudoterminal = { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: () => closeEmitter.fire(0), + close: () => { } + }; + const terminal = window.createTerminal({ name: 'foo', pty }); + }); + + test('exitStatus.code should be set to the exit code (non-zero)', (done) => { + disposables.push(window.onDidOpenTerminal(term => { + try { + equal(terminal, term); + equal(terminal.exitStatus, undefined); + } catch (e) { + done(e); + } + disposables.push(window.onDidCloseTerminal(t => { + try { + equal(terminal, t); + deepEqual(terminal.exitStatus, { code: 22 }); + } catch (e) { + done(e); + return; + } + done(); + })); + })); + const writeEmitter = new EventEmitter(); + const closeEmitter = new EventEmitter(); + const pty: Pseudoterminal = { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: () => closeEmitter.fire(22), close: () => { } }; const terminal = window.createTerminal({ name: 'foo', pty }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 2a268d8cf3a914d2cc6a12391a037eafb3429495..2edca691968802ac88ba2630006173de68a250da 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -674,6 +674,17 @@ declare module 'vscode' { readonly data: string; } + export interface TerminalExitStatus { + /** + * The exit code that a terminal exited with, it can have the following values: + * - Zero: the terminal process or custom execution succeeded. + * - Non-zero: the terminal process or custom execution failed. + * - `undefined`: the user forcefully closed the terminal or a custom execution exited + * without providing an exit code. + */ + readonly code: number | undefined; + } + namespace window { /** * An event which fires when the [dimensions](#Terminal.dimensions) of the terminal change. @@ -695,6 +706,21 @@ declare module 'vscode' { * created. */ readonly dimensions: TerminalDimensions | undefined; + + /** + * The exit status of the terminal, this will be undefined while the terminal is active. + * + * **Example:** Show a notification with the exit code when the terminal exits with a + * non-zero exit code. + * ```typescript + * window.onDidCloseTerminal(t => { + * if (t.exitStatus && t.exitStatus.code) { + * vscode.window.showInformationMessage(`Exit code: ${t.exitStatus.code}`); + * } + * }); + * ``` + */ + readonly exitStatus: TerminalExitStatus | undefined; } //#endregion diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 8344b323c7e5c6f7454ce1cfc43a8397de2d91b8..f9207e5226a0535d810d57aa95292732bb839e01 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -163,7 +163,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } private _onTerminalDisposed(terminalInstance: ITerminalInstance): void { - this._proxy.$acceptTerminalClosed(terminalInstance.id); + this._proxy.$acceptTerminalClosed(terminalInstance.id, terminalInstance.exitCode); } private _onTerminalOpened(terminalInstance: ITerminalInstance): void { @@ -257,7 +257,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._getTerminalProcess(terminalId).then(e => e.emitReady(pid, cwd)); } - public $sendProcessExit(terminalId: number, exitCode: number): void { + public $sendProcessExit(terminalId: number, exitCode: number | undefined): void { this._getTerminalProcess(terminalId).then(e => e.emitExit(exitCode)); this._terminalProcesses.delete(terminalId); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 586824df3bbd6b0bd6e4bb80b66b58fa58a2023a..231421e80f0a58087d4f30ed491fc474acb290cc 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -420,7 +420,7 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $sendProcessTitle(terminalId: number, title: string): void; $sendProcessData(terminalId: number, data: string): void; $sendProcessReady(terminalId: number, pid: number, cwd: string): void; - $sendProcessExit(terminalId: number, exitCode: number): void; + $sendProcessExit(terminalId: number, exitCode: number | undefined): void; $sendProcessInitialCwd(terminalId: number, cwd: string): void; $sendProcessCwd(terminalId: number, initialCwd: string): void; $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void; @@ -1196,7 +1196,7 @@ export interface ITerminalDimensionsDto { } export interface ExtHostTerminalServiceShape { - $acceptTerminalClosed(id: number): void; + $acceptTerminalClosed(id: number, exitCode: number | undefined): void; $acceptTerminalOpened(id: number, name: string): void; $acceptActiveTerminalChanged(id: number | null): void; $acceptTerminalProcessId(id: number, processId: number): void; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 40ecebf643b216bc6fd9944da202c4e2a610adac..0ccfc7d016ab99fde12da08825d2193aefc342ef 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -97,6 +97,7 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi private _cols: number | undefined; private _pidPromiseComplete: ((value: number | undefined) => any) | undefined; private _rows: number | undefined; + private _exitStatus: vscode.TerminalExitStatus | undefined; public isOpen: boolean = false; @@ -138,6 +139,10 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi this._name = name; } + public get exitStatus(): vscode.TerminalExitStatus | undefined { + return this._exitStatus; + } + public get dimensions(): vscode.TerminalDimensions | undefined { if (this._cols === undefined || this._rows === undefined) { return undefined; @@ -148,6 +153,10 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi }; } + public setExitCode(code: number | undefined) { + this._exitStatus = Object.freeze({ code }); + } + public setDimensions(cols: number, rows: number): boolean { if (cols === this._cols && rows === this._rows) { // Nothing changed @@ -210,8 +219,8 @@ class ApiRequest { export class ExtHostPseudoterminal implements ITerminalChildProcess { private readonly _onProcessData = new Emitter(); public readonly onProcessData: Event = this._onProcessData.event; - private readonly _onProcessExit = new Emitter(); - public readonly onProcessExit: Event = this._onProcessExit.event; + private readonly _onProcessExit = new Emitter(); + public readonly onProcessExit: Event = this._onProcessExit.event; private readonly _onProcessReady = new Emitter<{ pid: number, cwd: string }>(); public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = new Emitter(); @@ -253,7 +262,9 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess { // Attach the listeners this._pty.onDidWrite(e => this._onProcessData.fire(e)); if (this._pty.onDidClose) { - this._pty.onDidClose(e => this._onProcessExit.fire(e || 0)); + this._pty.onDidClose((e: number | undefined = undefined) => { + this._onProcessExit.fire(e); + }); } if (this._pty.onDidOverrideDimensions) { this._pty.onDidOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e ? { cols: e.columns, rows: e.rows } : e)); @@ -381,11 +392,12 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ } } - public async $acceptTerminalClosed(id: number): Promise { + public async $acceptTerminalClosed(id: number, exitCode: number | undefined): Promise { await this._getTerminalByIdEventually(id); const index = this._getTerminalObjectIndexById(this.terminals, id); if (index !== null) { const terminal = this._terminals.splice(index, 1)[0]; + terminal.setExitCode(exitCode); this._onDidCloseTerminal.fire(terminal); } } @@ -511,7 +523,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ return id; } - private _onProcessExit(id: number, exitCode: number): void { + private _onProcessExit(id: number, exitCode: number | undefined): void { this._bufferer.stopBuffering(id); // Remove process reference diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index ddad1d88a75642c79831da9c667c4e42145ba291..6b0661692ad3260ac73d4bb4d66373dc969337dd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -236,6 +236,8 @@ export interface ITerminalInstance { */ onExit: Event; + readonly exitCode: number | undefined; + processReady: Promise; /** diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 373f411253510786cdc35b3b9c6bc83198d2cdda..3cc02fb30b2ad607b4ee3310997b3414d39bd37f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -181,6 +181,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _hadFocusOnExit: boolean; private _isVisible: boolean; private _isDisposed: boolean; + private _exitCode: number | undefined; private _skipTerminalCommands: string[]; private _shellType: TerminalShellType; private _title: string = ''; @@ -227,6 +228,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // TODO: How does this work with detached processes? // TODO: Should this be an event as it can fire twice? public get processReady(): Promise { return this._processManager.ptyProcessReady; } + public get exitCode(): number | undefined { return this._exitCode; } public get title(): string { return this._title; } public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } public get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; } @@ -1011,6 +1013,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${exitCode}`); + this._exitCode = exitCode; this._isExiting = true; let exitCodeMessage: string | undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index 3db8f795da5f336a298aa97056b4dfe2b31c826e..eee8ddafca535d479e649f05eca9590c3d237862 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -17,8 +17,8 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal private readonly _onProcessData = this._register(new Emitter()); public readonly onProcessData: Event = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter()); - public readonly onProcessExit: Event = this._onProcessExit.event; + private readonly _onProcessExit = this._register(new Emitter()); + public readonly onProcessExit: Event = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } private readonly _onProcessTitleChanged = this._register(new Emitter()); @@ -87,7 +87,7 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal this._onProcessReady.fire({ pid, cwd }); } - public emitExit(exitCode: number): void { + public emitExit(exitCode: number | undefined): void { this._onProcessExit.fire(exitCode); this.dispose(); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index d221c1bf8c8805472241388ba466968eec2f9295..4a278f48228ee32d7bcf87d5571e3555f465e83b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -67,8 +67,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce public get onProcessData(): Event { return this._onProcessData.event; } private readonly _onProcessTitle = this._register(new Emitter()); public get onProcessTitle(): Event { return this._onProcessTitle.event; } - private readonly _onProcessExit = this._register(new Emitter()); - public get onProcessExit(): Event { return this._onProcessExit.event; } + private readonly _onProcessExit = this._register(new Emitter()); + public get onProcessExit(): Event { return this._onProcessExit.event; } private readonly _onProcessOverrideDimensions = this._register(new Emitter()); public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } private readonly _onProcessOverrideShellLaunchConfig = this._register(new Emitter()); @@ -285,7 +285,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce return Promise.resolve(this._latency); } - private _onExit(exitCode: number): void { + private _onExit(exitCode: number | undefined): void { this._process = null; // If the process is marked as launching then mark the process as killed diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index fdbc74996a0d792858df310c6f8a1956d270ddf4..8527a18f6c4a8e926c968bf3cf7652ac88a22f0d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -285,7 +285,7 @@ export interface ITerminalProcessManager extends IDisposable { readonly onBeforeProcessData: Event; readonly onProcessData: Event; readonly onProcessTitle: Event; - readonly onProcessExit: Event; + readonly onProcessExit: Event; readonly onProcessOverrideDimensions: Event; readonly onProcessResolvedShellLaunchConfig: Event; @@ -324,7 +324,7 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { emitData(data: string): void; emitTitle(title: string): void; emitReady(pid: number, cwd: string): void; - emitExit(exitCode: number): void; + emitExit(exitCode: number | undefined): void; emitOverrideDimensions(dimensions: ITerminalDimensions | undefined): void; emitResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void; emitInitialCwd(initialCwd: string): void; @@ -388,7 +388,7 @@ export interface IWindowsShellHelper extends IDisposable { */ export interface ITerminalChildProcess { onProcessData: Event; - onProcessExit: Event; + onProcessExit: Event; onProcessReady: Event<{ pid: number, cwd: string }>; onProcessTitleChanged: Event; onProcessOverrideDimensions?: Event;