Skip to content

Commit

Permalink
Automatically relaunch terminals when pty host is restarted
Browse files Browse the repository at this point in the history
Fixes #117548
  • Loading branch information
Tyriar committed Mar 19, 2021
1 parent 7727e93 commit f514652
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 25 deletions.
6 changes: 3 additions & 3 deletions src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,15 +395,15 @@ export interface ITerminalDimensions {
/**
* The columns of the terminal.
*/
readonly cols: number;
cols: number;

/**
* The rows of the terminal.
*/
readonly rows: number;
rows: number;
}

export interface ITerminalDimensionsOverride extends ITerminalDimensions {
export interface ITerminalDimensionsOverride extends Readonly<ITerminalDimensions> {
/**
* indicate that xterm must receive these exact dimensions, even if they overflow the ui!
*/
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/terminal/node/ptyHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ export class PtyHostService extends Disposable implements IPtyService {
);
this._onPtyHostStart.fire();

// Setup heartbeat service and trigger a heartbeat immediately to reset the timeouts
const heartbeatService = ProxyChannel.toService<IHeartbeatService>(client.getChannel(TerminalIpcChannels.Heartbeat));
heartbeatService.onBeat(() => this._handleHeartbeat());
this._handleHeartbeat();

// Handle exit
this._register(client.onDidProcessExit(e => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal
// Attach pty host listeners
if (channel.onPtyHostExit) {
this._register(channel.onPtyHostExit(() => {
notificationService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`);
this._logService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`);
}));
}
let unresponsiveNotification: INotificationHandle | undefined;
Expand Down
6 changes: 2 additions & 4 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr
import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { IProductService } from 'vs/platform/product/common/productService';
import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings';

// How long in milliseconds should an average frame take to render for a notification to appear
// which suggests the fallback DOM-based renderer
Expand Down Expand Up @@ -1065,10 +1066,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
xterm.writeln(exitCodeMessage);
}
if (typeof this._shellLaunchConfig.waitOnExit === 'string') {
let message = this._shellLaunchConfig.waitOnExit;
// Bold the message and add an extra new line to make it stand out from the rest of the output
message = `\r\n\x1b[1m${message}\x1b[0m`;
xterm.writeln(message);
xterm.write(formatMessageForTerminal(this._shellLaunchConfig.waitOnExit));
}
// Disable all input if the terminal is exiting and listen for next keypress
xterm.setOption('disableStdin', true);
Expand Down
47 changes: 37 additions & 10 deletions src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } fr
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { URI } from 'vs/base/common/uri';
import { IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, TerminalShellType, ILocalTerminalService, IOffProcessTerminalService } from 'vs/platform/terminal/common/terminal';
import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, TerminalShellType, ILocalTerminalService, IOffProcessTerminalService, ITerminalDimensions } from 'vs/platform/terminal/common/terminal';
import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
import { localize } from 'vs/nls';
import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings';

/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;
Expand Down Expand Up @@ -74,7 +76,10 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
private _ptyResponsiveListener: IDisposable | undefined;
private _ptyListenersAttached: boolean = false;
private _dataFilter: SeamlessRelaunchDataFilter;
private _isFeatureTerminal: boolean = false;

private _shellLaunchConfig?: IShellLaunchConfig;
private _dimensions: ITerminalDimensions = { cols: 0, rows: 0 };
private _isScreenReaderModeEnabled: boolean = false;

private readonly _onPtyDisconnect = this._register(new Emitter<void>());
public get onPtyDisconnect(): Event<void> { return this._onPtyDisconnect.event; }
Expand Down Expand Up @@ -178,7 +183,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
isScreenReaderModeEnabled: boolean,
reset: boolean = true
): Promise<ITerminalLaunchError | undefined> {
this._isFeatureTerminal = !!shellLaunchConfig.isFeatureTerminal;
this._shellLaunchConfig = shellLaunchConfig;
this._dimensions.cols = cols;
this._dimensions.rows = rows;
this._isScreenReaderModeEnabled = isScreenReaderModeEnabled;

if (shellLaunchConfig.isExtensionCustomPtyTerminal) {
this._processType = ProcessType.ExtensionTerminal;
this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._instanceId, shellLaunchConfig, cols, rows);
Expand Down Expand Up @@ -300,6 +309,13 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
public async relaunch(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, isScreenReaderModeEnabled: boolean, reset: boolean): Promise<ITerminalLaunchError | undefined> {
this.ptyProcessReady = this._createPtyProcessReadyPromise();
this._logService.trace(`Relaunching terminal instance ${this._instanceId}`);

// Fire reconnect if needed to ensure the terminal is usable again
if (this.isDisconnected) {
this.isDisconnected = false;
this._onPtyReconnect.fire();
}

return this.createProcess(shellLaunchConfig, cols, rows, isScreenReaderModeEnabled, reset);
}

Expand Down Expand Up @@ -398,20 +414,29 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce

// When the pty host restarts, reconnect is no longer possible so dispose the responsive
// listener
this._register(offProcessTerminalService.onPtyHostRestart(() => {
this._register(offProcessTerminalService.onPtyHostRestart(async () => {
// When the pty host restarts, reconnect is no longer possible
if (!this.isDisconnected) {
this.isDisconnected = true;
this._onPtyDisconnect.fire();
}
this._ptyResponsiveListener?.dispose();
this._ptyResponsiveListener = undefined;
// Indicate the process is exited (and gone forever) only for feature terminals so they
// can react to the exit, this is particularly important for tasks so that it knows that
// the process is not still active. Note that this is not done for regular terminals
// because otherwise the terminal instance would be disposed
if (this._isFeatureTerminal) {
this._onExit(undefined);
if (this._shellLaunchConfig) {
if (this._shellLaunchConfig.isFeatureTerminal) {
// Indicate the process is exited (and gone forever) only for feature terminals
// so they can react to the exit, this is particularly important for tasks so
// that it knows that the process is not still active. Note that this is not
// done for regular terminals because otherwise the terminal instance would be
// disposed.
this._onExit(undefined);
} else {
// For normal terminals write a message indicating what happened and relaunch
// using the previous shellLaunchConfig
let message = localize('ptyHostRelaunch', "Restarting the terminal because the connection to the shell process was lost...");
this._onProcessData.fire({ data: formatMessageForTerminal(message), sync: false });
await this.relaunch(this._shellLaunchConfig, this._dimensions.cols, this._dimensions.rows, this._isScreenReaderModeEnabled, false);
}
}
}));
}
Expand Down Expand Up @@ -442,6 +467,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
throw (error);
}
}
this._dimensions.cols = cols;
this._dimensions.rows = rows;
}

public async write(data: string): Promise<void> {
Expand Down
12 changes: 12 additions & 0 deletions src/vs/workbench/contrib/terminal/common/terminalStrings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/**
* Formats a message from the product to be written to the terminal.
*/
export function formatMessageForTerminal(message: string): string {
// Wrap in bold and ensure it's on a new line
return `\r\n\x1b[1m${message}\x1b[0m\n\r`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ export class LocalPty extends Disposable implements ITerminalChildProcess {
@ILocalPtyService private readonly _localPtyService: ILocalPtyService
) {
super();

if (this._localPtyService.onPtyHostExit) {
this._localPtyService.onPtyHostExit(() => {
this._onProcessExit.fire(undefined);
});
}
}

start(): Promise<ITerminalLaunchError | undefined> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe
// Attach pty host listeners
if (this._localPtyService.onPtyHostExit) {
this._register(this._localPtyService.onPtyHostExit(() => {
notificationService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`);
this._logService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`);
}));
}
let unresponsiveNotification: INotificationHandle | undefined;
Expand Down

0 comments on commit f514652

Please sign in to comment.