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

Support Terminal.exitStatus API #84000

Merged
merged 2 commits into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 112 additions & 3 deletions extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string>();
const closeEmitter = new EventEmitter<number | undefined>();
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<string>();
const closeEmitter = new EventEmitter<number | undefined>();
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<string>();
const closeEmitter = new EventEmitter<number | undefined>();
const pty: Pseudoterminal = {
onDidWrite: writeEmitter.event,
onDidClose: closeEmitter.event,
open: () => closeEmitter.fire(22),
close: () => { }
};
const terminal = window.createTerminal({ name: 'foo', pty });
Expand Down
26 changes: 26 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/browser/mainThreadTerminalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 17 additions & 5 deletions src/vs/workbench/api/common/extHostTerminalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -210,8 +219,8 @@ class ApiRequest {
export class ExtHostPseudoterminal implements ITerminalChildProcess {
private readonly _onProcessData = new Emitter<string>();
public readonly onProcessData: Event<string> = this._onProcessData.event;
private readonly _onProcessExit = new Emitter<number>();
public readonly onProcessExit: Event<number> = this._onProcessExit.event;
private readonly _onProcessExit = new Emitter<number | undefined>();
public readonly onProcessExit: Event<number | undefined> = 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<string>();
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -381,11 +392,12 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
}
}

public async $acceptTerminalClosed(id: number): Promise<void> {
public async $acceptTerminalClosed(id: number, exitCode: number | undefined): Promise<void> {
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);
}
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ export interface ITerminalInstance {
*/
onExit: Event<number | undefined>;

readonly exitCode: number | undefined;

processReady: Promise<void>;

/**
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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<void> { 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; }
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal

private readonly _onProcessData = this._register(new Emitter<string>());
public readonly onProcessData: Event<string> = this._onProcessData.event;
private readonly _onProcessExit = this._register(new Emitter<number>());
public readonly onProcessExit: Event<number> = this._onProcessExit.event;
private readonly _onProcessExit = this._register(new Emitter<number | undefined>());
public readonly onProcessExit: Event<number | undefined> = 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<string>());
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessTitle = this._register(new Emitter<string>());
public get onProcessTitle(): Event<string> { return this._onProcessTitle.event; }
private readonly _onProcessExit = this._register(new Emitter<number>());
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
private readonly _onProcessExit = this._register(new Emitter<number | undefined>());
public get onProcessExit(): Event<number | undefined> { return this._onProcessExit.event; }
private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensions | undefined>());
public get onProcessOverrideDimensions(): Event<ITerminalDimensions | undefined> { return this._onProcessOverrideDimensions.event; }
private readonly _onProcessOverrideShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>());
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/contrib/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export interface ITerminalProcessManager extends IDisposable {
readonly onBeforeProcessData: Event<IBeforeProcessDataEvent>;
readonly onProcessData: Event<string>;
readonly onProcessTitle: Event<string>;
readonly onProcessExit: Event<number>;
readonly onProcessExit: Event<number | undefined>;
readonly onProcessOverrideDimensions: Event<ITerminalDimensions | undefined>;
readonly onProcessResolvedShellLaunchConfig: Event<IShellLaunchConfig>;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -388,7 +388,7 @@ export interface IWindowsShellHelper extends IDisposable {
*/
export interface ITerminalChildProcess {
onProcessData: Event<string>;
onProcessExit: Event<number>;
onProcessExit: Event<number | undefined>;
onProcessReady: Event<{ pid: number, cwd: string }>;
onProcessTitleChanged: Event<string>;
onProcessOverrideDimensions?: Event<ITerminalDimensions | undefined>;
Expand Down