Skip to content

Commit f9e6b35

Browse files
committed
Recover on pty host reconnect
Fixes #117990
1 parent bd65564 commit f9e6b35

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
@@ -340,7 +340,9 @@ export class TerminalService implements ITerminalService {
340340
}
341341

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

346348
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>;

0 commit comments

Comments
 (0)