Skip to content

Commit 3be37a7

Browse files
authored
Merge pull request #118438 from microsoft/tyriar/1_54_117990
Recover on pty host reconnect
2 parents d461d2f + f9e6b35 commit 3be37a7

File tree

11 files changed

+113
-32
lines changed

11 files changed

+113
-32
lines changed

src/vs/platform/terminal/common/terminal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface IPtyService {
7171
readonly onPtyHostExit?: Event<number>;
7272
readonly onPtyHostStart?: Event<void>;
7373
readonly onPtyHostUnresponsive?: Event<void>;
74+
readonly onPtyHostResponsive?: Event<void>;
7475

7576
readonly onProcessData: Event<{ id: number, event: IProcessDataEvent | string }>;
7677
readonly onProcessExit: Event<{ id: number, event: number | undefined }>;

src/vs/platform/terminal/electron-browser/localPtyService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export class LocalPtyService extends Disposable implements IPtyService {
4343
readonly onPtyHostStart = this._onPtyHostStart.event;
4444
private readonly _onPtyHostUnresponsive = this._register(new Emitter<void>());
4545
readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event;
46+
private readonly _onPtyHostResponsive = this._register(new Emitter<void>());
47+
readonly onPtyHostResponsive = this._onPtyHostResponsive.event;
4648

4749
private readonly _onProcessData = this._register(new Emitter<{ id: number, event: IProcessDataEvent | string }>());
4850
readonly onProcessData = this._onProcessData.event;
@@ -186,6 +188,7 @@ export class LocalPtyService extends Disposable implements IPtyService {
186188
private _handleHeartbeat() {
187189
this._clearHeartbeatTimeouts();
188190
this._heartbeatFirstTimeout = setTimeout(() => this._handleHeartbeatFirstTimeout(), HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier);
191+
this._onPtyHostResponsive.fire();
189192
}
190193

191194
private _handleHeartbeatFirstTimeout() {

src/vs/workbench/contrib/terminal/browser/terminal.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ export interface ITerminalInstanceService {
3636
* terminals using this pty host connection and mark them as disconnected.
3737
*/
3838
onPtyHostUnresponsive: Event<void>;
39+
/**
40+
* Fired when the ptyHost process becomes responsive after being non-responsive. Allowing
41+
* previously disconnected terminals to reconnect.
42+
*/
43+
onPtyHostResponsive: Event<void>;
44+
/**
45+
* Fired when the ptyHost has been restarted, this is used as a signal for listening terminals
46+
* that its pty has been lost and will remain disconnected.
47+
*/
48+
onPtyHostRestart: Event<void>;
3949

4050
// These events are optional as the requests they make are only needed on the browser side
4151
onRequestDefaultShellAndArgs?: Event<IDefaultShellAndArgsRequest>;
@@ -287,6 +297,11 @@ export interface ITerminalInstance {
287297
*/
288298
readonly shouldPersist: boolean;
289299

300+
/**
301+
* Whether the process communication channel has been disconnected.
302+
*/
303+
readonly isDisconnected: boolean;
304+
290305
/**
291306
* An event that fires when the terminal instance's title changes.
292307
*/

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
156156
public get shellType(): TerminalShellType { return this._shellType; }
157157
public get commandTracker(): CommandTrackerAddon | undefined { return this._commandTrackerAddon; }
158158
public get navigationMode(): INavigationMode | undefined { return this._navigationModeAddon; }
159+
public get isDisconnected(): boolean { return this._processManager.isDisconnected; }
159160

160161
private readonly _onExit = new Emitter<number | undefined>();
161162
public get onExit(): Event<number | undefined> { return this._onExit.event; }
@@ -957,8 +958,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
957958
this._processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e));
958959
this._processManager.onPtyDisconnect(() => {
959960
this._safeSetOption('disableStdin', true);
960-
// Use api source so it cannot be overridden
961-
this.setTitle(nls.localize('ptyDisconnected', "{0} (disconnected)", this._title), TitleEventSource.Api);
961+
this._onTitleChanged.fire(this);
962+
});
963+
this._processManager.onPtyReconnect(() => {
964+
this._safeSetOption('disableStdin', false);
965+
this._onTitleChanged.fire(this);
962966
});
963967

964968
if (this._shellLaunchConfig.name) {

src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search';
1010
import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11';
1111
import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl';
1212
import { IProcessEnvironment } from 'vs/base/common/platform';
13-
import { Emitter } from 'vs/base/common/event';
13+
import { Emitter, Event } from 'vs/base/common/event';
1414
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
1515
import { Disposable } from 'vs/base/common/lifecycle';
1616
import { ITerminalsLayoutInfoById, ITerminalsLayoutInfo, ITerminalChildProcess } from 'vs/platform/terminal/common/terminal';
@@ -24,10 +24,10 @@ let WebglAddon: typeof XTermWebglAddon;
2424
export class TerminalInstanceService extends Disposable implements ITerminalInstanceService {
2525
public _serviceBrand: undefined;
2626

27-
private readonly _onPtyHostExit = this._register(new Emitter<void>());
28-
readonly onPtyHostExit = this._onPtyHostExit.event;
29-
private readonly _onPtyHostUnresponsive = this._register(new Emitter<void>());
30-
readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event;
27+
readonly onPtyHostExit = Event.None;
28+
readonly onPtyHostUnresponsive = Event.None;
29+
readonly onPtyHostResponsive = Event.None;
30+
readonly onPtyHostRestart = Event.None;
3131
private readonly _onRequestDefaultShellAndArgs = this._register(new Emitter<IDefaultShellAndArgsRequest>());
3232
readonly onRequestDefaultShellAndArgs = this._onRequestDefaultShellAndArgs.event;
3333

src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
2121
import { IRemoteTerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
2222
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2323
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
24-
import { Disposable } from 'vs/base/common/lifecycle';
24+
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
2525
import { withNullAsUndefined } from 'vs/base/common/types';
2626
import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo';
2727
import { IPathService } from 'vs/workbench/services/path/common/pathService';
@@ -57,6 +57,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
5757
public remoteAuthority: string | undefined;
5858
public os: platform.OperatingSystem | undefined;
5959
public userHome: string | undefined;
60+
public isDisconnected: boolean = false;
6061

6162
private _process: ITerminalChildProcess | null = null;
6263
private _processType: ProcessType = ProcessType.Process;
@@ -68,9 +69,13 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
6869
private _environmentVariableInfo: IEnvironmentVariableInfo | undefined;
6970
private _ackDataBufferer: AckDataBufferer;
7071
private _hasWrittenData: boolean = false;
72+
private _ptyResponsiveListener: IDisposable | undefined;
7173

7274
private readonly _onPtyDisconnect = this._register(new Emitter<void>());
7375
public get onPtyDisconnect(): Event<void> { return this._onPtyDisconnect.event; }
76+
private readonly _onPtyReconnect = this._register(new Emitter<void>());
77+
public get onPtyReconnect(): Event<void> { return this._onPtyReconnect.event; }
78+
7479
private readonly _onProcessReady = this._register(new Emitter<void>());
7580
public get onProcessReady(): Event<void> { return this._onProcessReady.event; }
7681
private readonly _onBeforeProcessData = this._register(new Emitter<IBeforeProcessDataEvent>());
@@ -328,7 +333,20 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
328333
const useConpty = this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled;
329334
const shouldPersist = this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isFeatureTerminal;
330335

331-
this._terminalInstanceService.onPtyHostUnresponsive(() => this._onPtyDisconnect.fire());
336+
this._register(this._terminalInstanceService.onPtyHostUnresponsive(() => {
337+
this.isDisconnected = true;
338+
this._onPtyDisconnect.fire();
339+
}));
340+
this._ptyResponsiveListener = this._terminalInstanceService.onPtyHostResponsive(() => {
341+
this.isDisconnected = false;
342+
this._onPtyReconnect.fire();
343+
});
344+
this._register(toDisposable(() => this._ptyResponsiveListener?.dispose()));
345+
this._register(this._terminalInstanceService.onPtyHostRestart(() => {
346+
// When the pty host restarts, reconnect is no longer possible
347+
this._ptyResponsiveListener?.dispose();
348+
this._ptyResponsiveListener = undefined;
349+
}));
332350
return await this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, useConpty, shouldPersist);
333351
}
334352

src/vs/workbench/contrib/terminal/browser/terminalService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,9 @@ export class TerminalService implements ITerminalService {
341341
}
342342

343343
public getTabLabels(): string[] {
344-
return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => `${index + 1}: ${tab.title ? tab.title : ''}`);
344+
return this._terminalTabs.filter(tab => tab.terminalInstances.length > 0).map((tab, index) => {
345+
return `${index + 1}: ${tab.title ? tab.title : ''}`;
346+
});
345347
}
346348

347349
public getFindState(): FindReplaceState {

src/vs/workbench/contrib/terminal/browser/terminalTab.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
1212
import { ITerminalInstance, Direction, ITerminalTab, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
1313
import { ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views';
1414
import { IShellLaunchConfig, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal';
15+
import { localize } from 'vs/nls';
1516

1617
const SPLIT_PANE_MIN_SIZE = 120;
1718

@@ -402,15 +403,20 @@ export class TerminalTab extends Disposable implements ITerminalTab {
402403
}
403404

404405
public get title(): string {
405-
let title = this.terminalInstances[0].title;
406+
let title = this._titleWithConnectionStatus(this.terminalInstances[0]);
406407
for (let i = 1; i < this.terminalInstances.length; i++) {
407-
if (this.terminalInstances[i].title) {
408-
title += `, ${this.terminalInstances[i].title}`;
408+
const instance = this.terminalInstances[i];
409+
if (instance.title) {
410+
title += `, ${this._titleWithConnectionStatus(instance)}`;
409411
}
410412
}
411413
return title;
412414
}
413415

416+
private _titleWithConnectionStatus(instance: ITerminalInstance): string {
417+
return instance.isDisconnected ? localize('ptyDisconnected', "{0} (disconnected)", instance.title) : instance.title;
418+
}
419+
414420
public setVisible(visible: boolean): void {
415421
this._isVisible = visible;
416422
if (this._tabElement) {

src/vs/workbench/contrib/terminal/browser/terminalView.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,15 @@ class SwitchTerminalActionViewItem extends SelectActionViewItem {
397397
}
398398

399399
function getTerminalSelectOpenItems(terminalService: ITerminalService, contributions: ITerminalContributionService): ISelectOptionItem[] {
400-
const items = terminalService.connectionState === TerminalConnectionState.Connected ?
401-
terminalService.getTabLabels().map(label => <ISelectOptionItem>{ text: label }) :
402-
[{ text: nls.localize('terminalConnectingLabel', "Starting...") }];
400+
let items: ISelectOptionItem[];
401+
402+
if (terminalService.connectionState === TerminalConnectionState.Connected) {
403+
items = terminalService.getTabLabels().map(label => {
404+
return { text: label };
405+
});
406+
} else {
407+
items = [{ text: nls.localize('terminalConnectingLabel', "Starting...") }];
408+
}
403409

404410
items.push({ text: switchTerminalActionViewItemSeparator, isDisabled: true });
405411

src/vs/workbench/contrib/terminal/common/terminal.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,13 @@ export interface ITerminalProcessManager extends IDisposable {
248248
readonly environmentVariableInfo: IEnvironmentVariableInfo | undefined;
249249
readonly persistentTerminalId: number | undefined;
250250
readonly shouldPersist: boolean;
251+
readonly isDisconnected: boolean;
251252
/** Whether the process has had data written to it yet. */
252253
readonly hasWrittenData: boolean;
253254

254255
readonly onPtyDisconnect: Event<void>;
256+
readonly onPtyReconnect: Event<void>;
257+
255258
readonly onProcessReady: Event<void>;
256259
readonly onBeforeProcessData: Event<IBeforeProcessDataEvent>;
257260
readonly onProcessData: Event<IProcessDataEvent>;

src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Emitter } from 'vs/base/common/event';
2828
import { Disposable } from 'vs/base/common/lifecycle';
2929
import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
3030
import { ILabelService } from 'vs/platform/label/common/label';
31-
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
31+
import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
3232
import { localize } from 'vs/nls';
3333

3434
let Terminal: typeof XTermTerminal;
@@ -40,11 +40,16 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst
4040
public _serviceBrand: undefined;
4141

4242
private readonly _ptys: Map<number, LocalPty> = new Map();
43+
private _isPtyHostUnresponsive: boolean = false;
4344

4445
private readonly _onPtyHostExit = this._register(new Emitter<void>());
4546
readonly onPtyHostExit = this._onPtyHostExit.event;
4647
private readonly _onPtyHostUnresponsive = this._register(new Emitter<void>());
4748
readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event;
49+
private readonly _onPtyHostResponsive = this._register(new Emitter<void>());
50+
readonly onPtyHostResponsive = this._onPtyHostResponsive.event;
51+
private readonly _onPtyHostRestart = this._register(new Emitter<void>());
52+
readonly onPtyHostRestart = this._onPtyHostRestart.event;
4853

4954
constructor(
5055
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@@ -61,41 +66,59 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst
6166
super();
6267

6368
// Attach process listeners
64-
this._localPtyService.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event));
65-
this._localPtyService.onProcessExit(e => {
69+
this._register(this._localPtyService.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event)));
70+
this._register(this._localPtyService.onProcessExit(e => {
6671
const pty = this._ptys.get(e.id);
6772
if (pty) {
6873
pty.handleExit(e.event);
6974
this._ptys.delete(e.id);
7075
}
71-
});
72-
this._localPtyService.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event));
73-
this._localPtyService.onProcessTitleChanged(e => this._ptys.get(e.id)?.handleTitleChanged(e.event));
74-
this._localPtyService.onProcessOverrideDimensions(e => this._ptys.get(e.id)?.handleOverrideDimensions(e.event));
75-
this._localPtyService.onProcessResolvedShellLaunchConfig(e => this._ptys.get(e.id)?.handleResolvedShellLaunchConfig(e.event));
76-
this._localPtyService.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event));
76+
}));
77+
this._register(this._localPtyService.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)));
78+
this._register(this._localPtyService.onProcessTitleChanged(e => this._ptys.get(e.id)?.handleTitleChanged(e.event)));
79+
this._register(this._localPtyService.onProcessOverrideDimensions(e => this._ptys.get(e.id)?.handleOverrideDimensions(e.event)));
80+
this._register(this._localPtyService.onProcessResolvedShellLaunchConfig(e => this._ptys.get(e.id)?.handleResolvedShellLaunchConfig(e.event)));
81+
this._register(this._localPtyService.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)));
7782

7883
// Attach pty host listeners
7984
if (this._localPtyService.onPtyHostExit) {
80-
this._localPtyService.onPtyHostExit(e => {
85+
this._register(this._localPtyService.onPtyHostExit(() => {
8186
this._onPtyHostExit.fire();
8287
notificationService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`);
83-
});
88+
}));
8489
}
90+
let unresponsiveNotification: INotificationHandle | undefined;
8591
if (this._localPtyService.onPtyHostStart) {
86-
this._localPtyService.onPtyHostStart(() => {
92+
this._register(this._localPtyService.onPtyHostStart(() => {
8793
this._logService.info(`ptyHost restarted`);
88-
});
94+
this._onPtyHostRestart.fire();
95+
unresponsiveNotification?.close();
96+
unresponsiveNotification = undefined;
97+
this._isPtyHostUnresponsive = false;
98+
}));
8999
}
90100
if (this._localPtyService.onPtyHostUnresponsive) {
91-
this._localPtyService.onPtyHostUnresponsive(() => {
101+
this._register(this._localPtyService.onPtyHostUnresponsive(() => {
92102
const choices: IPromptChoice[] = [{
93103
label: localize('restartPtyHost', "Restart pty host"),
94104
run: () => this._localPtyService.restartPtyHost!()
95105
}];
96-
notificationService.prompt(Severity.Error, localize('nonResponsivePtyHost', "The connection to the terminal's pty host process is unresponsive, the terminals may stop working."), choices);
106+
unresponsiveNotification = notificationService.prompt(Severity.Error, localize('nonResponsivePtyHost', "The connection to the terminal's pty host process is unresponsive, the terminals may stop working."), choices);
107+
this._isPtyHostUnresponsive = true;
97108
this._onPtyHostUnresponsive.fire();
98-
});
109+
}));
110+
}
111+
if (this._localPtyService.onPtyHostResponsive) {
112+
this._register(this._localPtyService.onPtyHostResponsive(() => {
113+
if (!this._isPtyHostUnresponsive) {
114+
return;
115+
}
116+
this._logService.info('The pty host became responsive again');
117+
unresponsiveNotification?.close();
118+
unresponsiveNotification = undefined;
119+
this._isPtyHostUnresponsive = false;
120+
this._onPtyHostResponsive.fire();
121+
}));
99122
}
100123
}
101124

0 commit comments

Comments
 (0)