Skip to content

Commit 56f97a4

Browse files
authored
Merge pull request microsoft#187096 from microsoft/tyriar/187076
Fix several pty host lifecycle issues on remotes
2 parents 535da93 + 3d6d839 commit 56f97a4

File tree

8 files changed

+77
-44
lines changed

8 files changed

+77
-44
lines changed

src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export class ElectronPtyHostStarter extends Disposable implements IPtyHostStarte
2323

2424
private utilityProcess: UtilityProcess | undefined = undefined;
2525

26-
private readonly _onBeforeWindowConnection = new Emitter<void>();
27-
readonly onBeforeWindowConnection = this._onBeforeWindowConnection.event;
26+
private readonly _onRequestConnection = new Emitter<void>();
27+
readonly onRequestConnection = this._onRequestConnection.event;
2828
private readonly _onWillShutdown = new Emitter<void>();
2929
readonly onWillShutdown = this._onWillShutdown.event;
3030

@@ -104,7 +104,7 @@ export class ElectronPtyHostStarter extends Disposable implements IPtyHostStarte
104104
}
105105

106106
private _onWindowConnection(e: IpcMainEvent, nonce: string) {
107-
this._onBeforeWindowConnection.fire();
107+
this._onRequestConnection.fire();
108108

109109
const port = this.utilityProcess!.connect();
110110

src/vs/platform/terminal/node/ptyHost.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface IPtyHostConnection {
1414
}
1515

1616
export interface IPtyHostStarter extends IDisposable {
17-
onBeforeWindowConnection?: Event<void>;
17+
onRequestConnection?: Event<void>;
1818
onWillShutdown?: Event<void>;
1919

2020
/**

src/vs/platform/terminal/node/ptyHostMain.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ async function startPtyHost() {
8888

8989
// Clean up
9090
process.once('exit', () => {
91+
logService.trace('Pty host exiting');
9192
logService.dispose();
9293
heartbeatService.dispose();
9394
ptyService.dispose();

src/vs/platform/terminal/node/ptyHostService.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
import { Emitter, Event } from 'vs/base/common/event';
77
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
8-
import { IProcessEnvironment, OperatingSystem, isWindows } from 'vs/base/common/platform';
8+
import { IProcessEnvironment, OS, OperatingSystem, isWindows } from 'vs/base/common/platform';
99
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
1010
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
11-
import { ILogService, ILoggerService } from 'vs/platform/log/common/log';
11+
import { ILogService, ILoggerService, LogLevel } from 'vs/platform/log/common/log';
1212
import { RemoteLoggerChannelClient } from 'vs/platform/log/common/logIpc';
1313
import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv';
1414
import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/capabilities/capabilities';
@@ -19,6 +19,7 @@ import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs
1919
import { IPtyHostConnection, IPtyHostStarter } from 'vs/platform/terminal/node/ptyHost';
2020
import { detectAvailableProfiles } from 'vs/platform/terminal/node/terminalProfiles';
2121
import * as performance from 'vs/base/common/performance';
22+
import { getSystemShell } from 'vs/base/node/shell';
2223

2324
enum Constants {
2425
MaxRestarts = 5
@@ -43,6 +44,13 @@ export class PtyHostService extends Disposable implements IPtyService {
4344
this._ensurePtyHost();
4445
return this.__proxy!;
4546
}
47+
/**
48+
* Get the proxy if it exists, otherwise undefined. This is used when calls are not needed to be
49+
* passed through to the pty host if it has not yet been spawned.
50+
*/
51+
private get _optionalProxy(): IPtyService | undefined {
52+
return this.__proxy;
53+
}
4654

4755
private _ensurePtyHost() {
4856
if (!this.__connection) {
@@ -102,12 +110,9 @@ export class PtyHostService extends Disposable implements IPtyService {
102110
this._resolveVariablesRequestStore = this._register(new RequestStore(undefined, this._logService));
103111
this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables);
104112

105-
// Force the pty host to start as the first window is starting if the starter has that
106-
// capability
107-
if (this._ptyHostStarter.onBeforeWindowConnection) {
108-
Event.once(this._ptyHostStarter.onBeforeWindowConnection)(() => this._ensurePtyHost());
109-
} else {
110-
this._ensurePtyHost();
113+
// Start the pty host when a window requests a connection, if the starter has that capability.
114+
if (this._ptyHostStarter.onRequestConnection) {
115+
Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost());
111116
}
112117

113118
this._ptyHostStarter.onWillShutdown?.(() => this._wasQuitRequested = true);
@@ -118,7 +123,7 @@ export class PtyHostService extends Disposable implements IPtyService {
118123
}
119124

120125
private async _refreshIgnoreProcessNames(): Promise<void> {
121-
return this._proxy.refreshIgnoreProcessNames?.(this._ignoreProcessNames);
126+
return this._optionalProxy?.refreshIgnoreProcessNames?.(this._ignoreProcessNames);
122127
}
123128

124129
private async _resolveShellEnv(): Promise<typeof process.env> {
@@ -139,6 +144,11 @@ export class PtyHostService extends Disposable implements IPtyService {
139144
const connection = this._ptyHostStarter.start();
140145
const client = connection.client;
141146

147+
// Log a full stack trace which will tell the exact reason the pty host is starting up
148+
if (this._logService.getLevel() === LogLevel.Trace) {
149+
this._logService.trace('PtyHostService#_startPtyHost', new Error().stack?.replace(/^Error/, ''));
150+
}
151+
142152
// Setup heartbeat service and trigger a heartbeat immediately to reset the timeouts
143153
const heartbeatService = ProxyChannel.toService<IHeartbeatService>(client.getChannel(TerminalIpcChannels.Heartbeat));
144154
heartbeatService.onBeat(() => this._handleHeartbeat());
@@ -224,13 +234,10 @@ export class PtyHostService extends Disposable implements IPtyService {
224234
return this._proxy.listProcesses();
225235
}
226236
async getPerformanceMarks(): Promise<performance.PerformanceMark[]> {
227-
if (!this.__proxy) {
228-
return [];
229-
}
230-
return this._proxy.getPerformanceMarks();
237+
return this._optionalProxy?.getPerformanceMarks() ?? [];
231238
}
232-
reduceConnectionGraceTime(): Promise<void> {
233-
return this._proxy.reduceConnectionGraceTime();
239+
async reduceConnectionGraceTime(): Promise<void> {
240+
return this._optionalProxy?.reduceConnectionGraceTime();
234241
}
235242
start(id: number): Promise<ITerminalLaunchError | { injectedArgs: string[] } | undefined> {
236243
return this._proxy.start(id);
@@ -280,7 +287,7 @@ export class PtyHostService extends Disposable implements IPtyService {
280287
}
281288

282289
getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string> {
283-
return this._proxy.getDefaultSystemShell(osOverride);
290+
return this._optionalProxy?.getDefaultSystemShell(osOverride) ?? getSystemShell(osOverride ?? OS, process.env);
284291
}
285292
async getProfiles(workspaceId: string, profiles: unknown, defaultProfile: unknown, includeDetectedProfiles: boolean = false): Promise<ITerminalProfile[]> {
286293
const shellEnv = await this._resolveShellEnv();

src/vs/platform/terminal/node/terminalProcess.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/co
1313
import { URI } from 'vs/base/common/uri';
1414
import { Promises } from 'vs/base/node/pfs';
1515
import { localize } from 'vs/nls';
16-
import { ILogService } from 'vs/platform/log/common/log';
16+
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
1717
import { IProductService } from 'vs/platform/product/common/productService';
1818
import { FlowControlConstants, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap as IProcessPropertyMap, ProcessPropertyType, TerminalShellType, IProcessReadyEvent, ITerminalProcessOptions, PosixShellType, IProcessReadyWindowsPty } from 'vs/platform/terminal/common/terminal';
1919
import { ChildProcessMonitor } from 'vs/platform/terminal/node/childProcessMonitor';
@@ -353,6 +353,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
353353
// Allow any trailing data events to be sent before the exit event is sent.
354354
// See https://github.com/Tyriar/node-pty/issues/72
355355
private _queueProcessExit() {
356+
if (this._logService.getLevel() === LogLevel.Trace) {
357+
this._logService.trace('TerminalProcess#_queueProcessExit', new Error().stack?.replace(/^Error/, ''));
358+
}
356359
if (this._closeTimeout) {
357360
clearTimeout(this._closeTimeout);
358361
}
@@ -417,6 +420,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
417420
}
418421

419422
shutdown(immediate: boolean): void {
423+
if (this._logService.getLevel() === LogLevel.Trace) {
424+
this._logService.trace('TerminalProcess#shutdown', new Error().stack?.replace(/^Error/, ''));
425+
}
420426
// don't force immediate disposal of the terminal processes on Windows as an additional
421427
// mitigation for https://github.com/microsoft/vscode/issues/71966 which causes the pty host
422428
// to become unresponsive, disconnecting all terminals across all windows.

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

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { DeferredPromise } from 'vs/base/common/async';
77
import { Emitter } from 'vs/base/common/event';
88
import { revive } from 'vs/base/common/marshalling';
9-
import { PerformanceMark } from 'vs/base/common/performance';
9+
import { PerformanceMark, mark } from 'vs/base/common/performance';
1010
import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
1111
import { ICommandService } from 'vs/platform/commands/common/commands';
1212
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -324,22 +324,28 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack
324324
// Revive processes if needed
325325
const serializedState = this._storageService.get(TerminalStorageKeys.TerminalBufferState, StorageScope.WORKSPACE);
326326
const parsed = this._deserializeTerminalState(serializedState);
327-
if (parsed) {
328-
try {
329-
// Note that remote terminals do not get their environment re-resolved unlike in local terminals
330-
331-
await this._remoteTerminalChannel.reviveTerminalProcesses(parsed, Intl.DateTimeFormat().resolvedOptions().locale);
332-
this._storageService.remove(TerminalStorageKeys.TerminalBufferState, StorageScope.WORKSPACE);
333-
// If reviving processes, send the terminal layout info back to the pty host as it
334-
// will not have been persisted on application exit
335-
const layoutInfo = this._storageService.get(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE);
336-
if (layoutInfo) {
337-
await this._remoteTerminalChannel.setTerminalLayoutInfo(JSON.parse(layoutInfo));
338-
this._storageService.remove(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE);
339-
}
340-
} catch {
341-
// no-op
327+
if (!parsed) {
328+
return undefined;
329+
}
330+
331+
try {
332+
// Note that remote terminals do not get their environment re-resolved unlike in local terminals
333+
334+
mark('code/terminal/willReviveTerminalProcessesRemote');
335+
await this._remoteTerminalChannel.reviveTerminalProcesses(parsed, Intl.DateTimeFormat().resolvedOptions().locale);
336+
mark('code/terminal/didReviveTerminalProcessesRemote');
337+
this._storageService.remove(TerminalStorageKeys.TerminalBufferState, StorageScope.WORKSPACE);
338+
// If reviving processes, send the terminal layout info back to the pty host as it
339+
// will not have been persisted on application exit
340+
const layoutInfo = this._storageService.get(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE);
341+
if (layoutInfo) {
342+
mark('code/terminal/willSetTerminalLayoutInfoRemote');
343+
await this._remoteTerminalChannel.setTerminalLayoutInfo(JSON.parse(layoutInfo));
344+
mark('code/terminal/didSetTerminalLayoutInfoRemote');
345+
this._storageService.remove(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE);
342346
}
347+
} catch (e: unknown) {
348+
this._logService.warn('RemoteTerminalBackend#getTerminalLayoutInfo Error', e && typeof e === 'object' && 'message' in e ? e.message : e);
343349
}
344350

345351
return this._remoteTerminalChannel.getTerminalLayoutInfo();

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2080,24 +2080,33 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
20802080
this._refreshEnvironmentVariableInfoWidgetState(info);
20812081
}
20822082

2083-
private _refreshEnvironmentVariableInfoWidgetState(info?: IEnvironmentVariableInfo): void {
2083+
private async _refreshEnvironmentVariableInfoWidgetState(info?: IEnvironmentVariableInfo): Promise<void> {
20842084
// Check if the status should exist
20852085
if (!info) {
20862086
this.statusList.remove(TerminalStatus.RelaunchNeeded);
20872087
this.statusList.remove(TerminalStatus.EnvironmentVariableInfoChangesActive);
20882088
return;
20892089
}
20902090

2091-
// Recreate the process if the terminal has not yet been interacted with and it's not a
2092-
// special terminal (eg. extension terminal)
2091+
// Recreate the process seamlessly without informing the use if the following conditions are
2092+
// met.
20932093
if (
2094+
// The change requires a relaunch
20942095
info.requiresAction &&
2096+
// The feature is enabled
20952097
this._configHelper.config.environmentChangesRelaunch &&
2098+
// Has not been interacted with
20962099
!this._processManager.hasWrittenData &&
2100+
// Not a feature terminal or is a reconnecting task terminal (TODO: Need to explain the latter case)
20972101
(!this._shellLaunchConfig.isFeatureTerminal || (this.reconnectionProperties && this._configurationService.getValue(TaskSettingId.Reconnection) === true)) &&
2098-
!this._shellLaunchConfig.customPtyImplementation
2099-
&& !this._shellLaunchConfig.isExtensionOwnedTerminal &&
2100-
!this._shellLaunchConfig.attachPersistentProcess
2102+
// Not a custom pty
2103+
!this._shellLaunchConfig.customPtyImplementation &&
2104+
// Not an extension owned terminal
2105+
!this._shellLaunchConfig.isExtensionOwnedTerminal &&
2106+
// Not a reconnected or revived terminal
2107+
!this._shellLaunchConfig.attachPersistentProcess &&
2108+
// Not a Windows remote using ConPTY (#187084)
2109+
!(this._processManager.remoteAuthority && this._configHelper.config.windowsEnableConpty && (await this._processManager.getBackendOS()) === OperatingSystem.Windows)
21012110
) {
21022111
this.relaunch();
21032112
return;

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ export class TerminalService implements ITerminalService {
422422
private _setConnected() {
423423
this._connectionState = TerminalConnectionState.Connected;
424424
this._onDidChangeConnectionState.fire();
425-
this._logService.trace('Reconnected to terminals');
425+
this._logService.trace('Pty host ready');
426426
}
427427

428428
private async _reconnectToRemoteTerminals(): Promise<void> {
@@ -444,6 +444,8 @@ export class TerminalService implements ITerminalService {
444444
// now that terminals have been restored,
445445
// attach listeners to update remote when terminals are changed
446446
this._attachProcessLayoutListeners();
447+
448+
this._logService.trace('Reconnected to remote terminals');
447449
}
448450

449451
private async _reconnectToLocalTerminals(): Promise<void> {
@@ -462,6 +464,8 @@ export class TerminalService implements ITerminalService {
462464
// now that terminals have been restored,
463465
// attach listeners to update local state when terminals are changed
464466
this._attachProcessLayoutListeners();
467+
468+
this._logService.trace('Reconnected to local terminals');
465469
}
466470

467471
private _recreateTerminalGroups(layoutInfo?: ITerminalsLayoutInfo): Promise<ITerminalGroup[]> {

0 commit comments

Comments
 (0)