Skip to content

Commit

Permalink
Don't connect LocalTerminalBackend until LifecyclePhase.Restored
Browse files Browse the repository at this point in the history
Fixes #175335
  • Loading branch information
Tyriar committed May 16, 2023
1 parent 3e4ed47 commit 13f1994
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,40 +35,37 @@ import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
import { acquirePort } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { mark } from 'vs/base/common/performance';
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { DeferredPromise } from 'vs/base/common/async';

export class LocalTerminalBackendContribution implements IWorkbenchContribution {
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@ILogService logService: ILogService,
@ITerminalInstanceService terminalInstanceService: ITerminalInstanceService
) {
mark('code/willConnectPtyHost');
logService.trace('Renderer->PtyHost#connect: before acquirePort');
acquirePort('vscode:createPtyHostMessageChannel', 'vscode:createPtyHostMessageChannelResult').then(port => {
mark('code/didConnectPtyHost');
logService.trace('Renderer->PtyHost#connect: connection established');

const backend = instantiationService.createInstance(LocalTerminalBackend, port);
Registry.as<ITerminalBackendRegistry>(TerminalExtensions.Backend).registerTerminalBackend(backend);
terminalInstanceService.didRegisterBackend(backend.remoteAuthority);
});
const backend = instantiationService.createInstance(LocalTerminalBackend);
Registry.as<ITerminalBackendRegistry>(TerminalExtensions.Backend).registerTerminalBackend(backend);
terminalInstanceService.didRegisterBackend(backend.remoteAuthority);
}
}

class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBackend {
readonly remoteAuthority = undefined;

private readonly _ptys: Map<number, LocalPty> = new Map();
// HACK: ProxyChannels behave strangely when interacting with promises and seemingly never
// resolve, so _proxyReady is awaited before every proxy use
private _proxy?: IPtyService;
private readonly _proxyReady: DeferredPromise<void>;

private _ptyHostDirectProxy: IPtyService;
private readonly _ptys: Map<number, LocalPty> = new Map();

private readonly _onDidRequestDetach = this._register(new Emitter<{ requestId: number; workspaceId: string; instanceId: number }>());
readonly onDidRequestDetach = this._onDidRequestDetach.event;

constructor(
port: MessagePort,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
@ILogService logService: ILogService,
@ILocalPtyService private readonly _localPtyService: ILocalPtyService,
@ILabelService private readonly _labelService: ILabelService,
Expand All @@ -86,81 +83,103 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke
) {
super(_localPtyService, logService, notificationService, historyService, _configurationResolverService, workspaceContextService);

// There are two connections to the pty host; one to the regular shared process
// _localPtyService, and one directly via message port _ptyHostDirectProxy. The former is
// used for pty host management messages, it would make sense in the future to use a
// separate interface/service for this one.
const client = new MessagePortClient(port, `window:${this._environmentService.window.id}`);
this._ptyHostDirectProxy = ProxyChannel.toService<IPtyService>(client.getChannel(TerminalIpcChannels.PtyHostWindow));

// Attach process listeners
this._ptyHostDirectProxy.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event));
this._ptyHostDirectProxy.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property));
this._ptyHostDirectProxy.onProcessExit(e => {
const pty = this._ptys.get(e.id);
if (pty) {
pty.handleExit(e.event);
this._ptys.delete(e.id);
}
});
this._ptyHostDirectProxy.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event));
this._ptyHostDirectProxy.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event));
this._ptyHostDirectProxy.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion());
this._ptyHostDirectProxy.onDidRequestDetach(e => this._onDidRequestDetach.fire(e));

// Listen for config changes
const initialConfig = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
for (const match of Object.keys(initialConfig.autoReplies)) {
// Ensure the reply is value
const reply = initialConfig.autoReplies[match] as string | null;
if (reply) {
this._ptyHostDirectProxy.installAutoReply(match, reply);
this._proxyReady = new DeferredPromise<void>();
this._connectToDirectProxy();
}

private async _connectToDirectProxy(): Promise<void> {
// The pty host should not get launched until the first window restored phase
await this._lifecycleService.when(LifecyclePhase.Restored);

mark('code/willConnectPtyHost');
this._logService.trace('Renderer->PtyHost#connect: before acquirePort');
acquirePort('vscode:createPtyHostMessageChannel', 'vscode:createPtyHostMessageChannelResult').then(port => {
mark('code/didConnectPtyHost');
this._logService.trace('Renderer->PtyHost#connect: connection established');
// There are two connections to the pty host; one to the regular shared process
// _localPtyService, and one directly via message port _ptyHostDirectProxy. The former is
// used for pty host management messages, it would make sense in the future to use a
// separate interface/service for this one.
const client = new MessagePortClient(port, `window:${this._environmentService.window.id}`);
const proxy = ProxyChannel.toService<IPtyService>(client.getChannel(TerminalIpcChannels.PtyHostWindow));
this._proxy = proxy;
this._proxyReady.complete();

// Attach process listeners
proxy.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event));
proxy.onDidChangeProperty(e => this._ptys.get(e.id)?.handleDidChangeProperty(e.property));
proxy.onProcessExit(e => {
const pty = this._ptys.get(e.id);
if (pty) {
pty.handleExit(e.event);
this._ptys.delete(e.id);
}
});
proxy.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event));
proxy.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event));
proxy.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion());
proxy.onDidRequestDetach(e => this._onDidRequestDetach.fire(e));

// Listen for config changes
const initialConfig = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
for (const match of Object.keys(initialConfig.autoReplies)) {
// Ensure the reply is value
const reply = initialConfig.autoReplies[match] as string | null;
if (reply) {
proxy.installAutoReply(match, reply);
}
}
}
// TODO: Could simplify update to a single call
this._register(this._configurationService.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(TerminalSettingId.AutoReplies)) {
this._ptyHostDirectProxy.uninstallAllAutoReplies();
const config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
for (const match of Object.keys(config.autoReplies)) {
// Ensure the reply is value
const reply = config.autoReplies[match] as string | null;
if (reply) {
await this._ptyHostDirectProxy.installAutoReply(match, reply);
// TODO: Could simplify update to a single call
this._register(this._configurationService.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(TerminalSettingId.AutoReplies)) {
proxy.uninstallAllAutoReplies();
const config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
for (const match of Object.keys(config.autoReplies)) {
// Ensure the reply is value
const reply = config.autoReplies[match] as string | null;
if (reply) {
proxy.installAutoReply(match, reply);
}
}
}
}
}));
}));
});
}

async requestDetachInstance(workspaceId: string, instanceId: number): Promise<IProcessDetails | undefined> {
return this._ptyHostDirectProxy.requestDetachInstance(workspaceId, instanceId);
await this._proxyReady.p;
return this._proxy!.requestDetachInstance(workspaceId, instanceId);
}

async acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise<void> {
if (!persistentProcessId) {
this._logService.warn('Cannot attach to feature terminals, custom pty terminals, or those without a persistentProcessId');
return;
}
return this._ptyHostDirectProxy.acceptDetachInstanceReply(requestId, persistentProcessId);
await this._proxyReady.p;
return this._proxy!.acceptDetachInstanceReply(requestId, persistentProcessId);
}

async persistTerminalState(): Promise<void> {
const ids = Array.from(this._ptys.keys());
const serialized = await this._ptyHostDirectProxy.serializeTerminalState(ids);
await this._proxyReady.p;
const serialized = await this._proxy!.serializeTerminalState(ids);
this._storageService.store(TerminalStorageKeys.TerminalBufferState, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE);
}

async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void> {
await this._ptyHostDirectProxy.updateTitle(id, title, titleSource);
await this._proxyReady.p;
await this._proxy!.updateTitle(id, title, titleSource);
}

async updateIcon(id: number, userInitiated: boolean, icon: URI | { light: URI; dark: URI } | { id: string; color?: { id: string } }, color?: string): Promise<void> {
await this._ptyHostDirectProxy.updateIcon(id, userInitiated, icon, color);
await this._proxyReady.p;
await this._proxy!.updateIcon(id, userInitiated, icon, color);
}

updateProperty<T extends ProcessPropertyType>(id: number, property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise<void> {
return this._ptyHostDirectProxy.updateProperty(id, property, value);
async updateProperty<T extends ProcessPropertyType>(id: number, property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise<void> {
await this._proxyReady.p;
return this._proxy!.updateProperty(id, property, value);
}

async createProcess(
Expand All @@ -174,15 +193,17 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke
shouldPersist: boolean
): Promise<ITerminalChildProcess> {
const executableEnv = await this._shellEnvironmentService.getShellEnv();
const id = await this._ptyHostDirectProxy.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, options, shouldPersist, this._getWorkspaceId(), this._getWorkspaceName());
await this._proxyReady.p;
const id = await this._proxy!.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, options, shouldPersist, this._getWorkspaceId(), this._getWorkspaceName());
const pty = this._instantiationService.createInstance(LocalPty, id, shouldPersist);
this._ptys.set(id, pty);
return pty;
}

async attachToProcess(id: number): Promise<ITerminalChildProcess | undefined> {
try {
await this._ptyHostDirectProxy.attachToProcess(id);
await this._proxyReady.p;
await this._proxy!.attachToProcess(id);
const pty = this._instantiationService.createInstance(LocalPty, id, true);
this._ptys.set(id, pty);
return pty;
Expand All @@ -194,7 +215,8 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke

async attachToRevivedProcess(id: number): Promise<ITerminalChildProcess | undefined> {
try {
const newId = await this._ptyHostDirectProxy.getRevivedPtyNewId(id) ?? id;
await this._proxyReady.p;
const newId = await this._proxy!.getRevivedPtyNewId(id) ?? id;
return await this.attachToProcess(newId);
} catch (e) {
this._logService.warn(`Couldn't attach to process ${e.message}`);
Expand All @@ -203,15 +225,18 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke
}

async listProcesses(): Promise<IProcessDetails[]> {
return this._ptyHostDirectProxy.listProcesses();
await this._proxyReady.p;
return this._proxy!.listProcesses();
}

async reduceConnectionGraceTime(): Promise<void> {
this._ptyHostDirectProxy.reduceConnectionGraceTime();
await this._proxyReady.p;
this._proxy!.reduceConnectionGraceTime();
}

async getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string> {
return this._ptyHostDirectProxy.getDefaultSystemShell(osOverride);
await this._proxyReady;
return this._proxy!.getDefaultSystemShell(osOverride);
}

async getProfiles(profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean) {
Expand All @@ -220,23 +245,26 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke
}

async getEnvironment(): Promise<IProcessEnvironment> {
return this._ptyHostDirectProxy.getEnvironment();
await this._proxyReady.p;
return this._proxy!.getEnvironment();
}

async getShellEnvironment(): Promise<IProcessEnvironment> {
return this._shellEnvironmentService.getShellEnv();
}

async getWslPath(original: string, direction: 'unix-to-win' | 'win-to-unix'): Promise<string> {
return this._ptyHostDirectProxy.getWslPath(original, direction);
await this._proxyReady.p;
return this._proxy!.getWslPath(original, direction);
}

async setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise<void> {
const args: ISetTerminalLayoutInfoArgs = {
workspaceId: this._getWorkspaceId(),
tabs: layoutInfo ? layoutInfo.tabs : []
};
await this._ptyHostDirectProxy.setTerminalLayoutInfo(args);
await this._proxyReady.p;
await this._proxy!.setTerminalLayoutInfo(args);
// Store in the storage service as well to be used when reviving processes as normally this
// is stored in memory on the pty host
this._storageService.store(TerminalStorageKeys.TerminalLayoutInfo, JSON.stringify(args), StorageScope.WORKSPACE, StorageTarget.MACHINE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { BaseTerminalProfileResolverService } from 'vs/workbench/contrib/termina
import { ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';

export class ElectronTerminalProfileResolverService extends BaseTerminalProfileResolverService {
Expand All @@ -23,7 +22,6 @@ export class ElectronTerminalProfileResolverService extends BaseTerminalProfileR
@IConfigurationResolverService configurationResolverService: IConfigurationResolverService,
@IConfigurationService configurationService: IConfigurationService,
@IHistoryService historyService: IHistoryService,
@ILifecycleService lifecycleService: ILifecycleService,
@ILogService logService: ILogService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@ITerminalProfileService terminalProfileService: ITerminalProfileService,
Expand Down

0 comments on commit 13f1994

Please sign in to comment.