Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up terminal reconnection #186072

Merged
merged 11 commits into from
Jun 26, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { deepStrictEqual, doesNotThrow, equal, strictEqual, throws } from 'assert';
import { deepStrictEqual, doesNotThrow, equal, ok, strictEqual, throws } from 'assert';
import { ConfigurationTarget, Disposable, env, EnvironmentVariableCollection, EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableMutatorType, EnvironmentVariableScope, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalExitReason, TerminalOptions, TerminalState, UIKind, Uri, window, workspace } from 'vscode';
import { assertNoRpc, poll } from '../utils';

Expand Down Expand Up @@ -368,11 +368,12 @@ import { assertNoRpc, poll } from '../utils';
try {
if (closeEvents.length === 1) {
deepStrictEqual(openEvents, ['test1']);
deepStrictEqual(dataEvents, [{ name: 'test1', data: 'write1' }]);
ok(dataEvents.some(e => e.name === 'test1' && e.data === 'write1'));
deepStrictEqual(closeEvents, ['test1']);
} else if (closeEvents.length === 2) {
deepStrictEqual(openEvents, ['test1', 'test2']);
deepStrictEqual(dataEvents, [{ name: 'test1', data: 'write1' }, { name: 'test2', data: 'write2' }]);
ok(dataEvents.some(e => e.name === 'test1' && e.data === 'write1'));
ok(dataEvents.some(e => e.name === 'test2' && e.data === 'write2'));
deepStrictEqual(closeEvents, ['test1', 'test2']);
}
resolveOnceClosed!();
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ export interface ITerminalChildProcess {

onProcessData: Event<IProcessDataEvent | string>;
onProcessReady: Event<IProcessReadyEvent>;
onProcessReplayComplete?: Event<void>;
onDidChangeProperty: Event<IProcessProperty<any>>;
onProcessExit: Event<number | undefined>;
onRestoreCommands?: Event<ISerializedCommandDetectionCapability>;
Expand Down
68 changes: 34 additions & 34 deletions src/vs/platform/terminal/node/ptyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,36 +204,42 @@ export class PtyService extends Disposable implements IPtyService {

@traceRpc
async reviveTerminalProcesses(state: ISerializedTerminalState[], dateTimeFormatLocale: string) {
const promises: Promise<void>[] = [];
for (const terminal of state) {
const restoreMessage = localize('terminal-history-restored', "History restored");
// TODO: We may at some point want to show date information in a hover via a custom sequence:
// new Date(terminal.timestamp).toLocaleDateString(dateTimeFormatLocale)
// new Date(terminal.timestamp).toLocaleTimeString(dateTimeFormatLocale)
const newId = await this.createProcess(
{
...terminal.shellLaunchConfig,
cwd: terminal.processDetails.cwd,
color: terminal.processDetails.color,
icon: terminal.processDetails.icon,
name: terminal.processDetails.titleSource === TitleEventSource.Api ? terminal.processDetails.title : undefined,
initialText: terminal.replayEvent.events[0].data + formatMessageForTerminal(restoreMessage, { loudFormatting: true })
},
terminal.processDetails.cwd,
terminal.replayEvent.events[0].cols,
terminal.replayEvent.events[0].rows,
terminal.unicodeVersion,
terminal.processLaunchConfig.env,
terminal.processLaunchConfig.executableEnv,
terminal.processLaunchConfig.options,
true,
terminal.processDetails.workspaceId,
terminal.processDetails.workspaceName,
true,
terminal.replayEvent.events[0].data
);
// Don't start the process here as there's no terminal to answer CPR
this._revivedPtyIdMap.set(terminal.id, { newId, state: terminal });
promises.push(this._reviveTerminalProcess(terminal));
}
await Promise.all(promises);
}

private async _reviveTerminalProcess(terminal: ISerializedTerminalState): Promise<void> {
const restoreMessage = localize('terminal-history-restored', "History restored");
// TODO: We may at some point want to show date information in a hover via a custom sequence:
// new Date(terminal.timestamp).toLocaleDateString(dateTimeFormatLocale)
// new Date(terminal.timestamp).toLocaleTimeString(dateTimeFormatLocale)
const newId = await this.createProcess(
{
...terminal.shellLaunchConfig,
cwd: terminal.processDetails.cwd,
color: terminal.processDetails.color,
icon: terminal.processDetails.icon,
name: terminal.processDetails.titleSource === TitleEventSource.Api ? terminal.processDetails.title : undefined,
initialText: terminal.replayEvent.events[0].data + formatMessageForTerminal(restoreMessage, { loudFormatting: true })
},
terminal.processDetails.cwd,
terminal.replayEvent.events[0].cols,
terminal.replayEvent.events[0].rows,
terminal.unicodeVersion,
terminal.processLaunchConfig.env,
terminal.processLaunchConfig.executableEnv,
terminal.processLaunchConfig.options,
true,
terminal.processDetails.workspaceId,
terminal.processDetails.workspaceName,
true,
terminal.replayEvent.events[0].data
);
// Don't start the process here as there's no terminal to answer CPR
this._revivedPtyIdMap.set(terminal.id, { newId, state: terminal });
}

@traceRpc
Expand Down Expand Up @@ -506,7 +512,6 @@ export class PtyService extends Disposable implements IPtyService {
if (layout) {
const expandedTabs = await Promise.all(layout.tabs.map(async tab => this._expandTerminalTab(tab)));
const tabs = expandedTabs.filter(t => t.terminals.length > 0);
this._logService.trace('PtyService.getTerminalLayoutInfo result', tabs);
performance.mark('code/didGetTerminalLayoutInfo');
return { tabs };
}
Expand Down Expand Up @@ -742,11 +747,6 @@ class PersistentTerminalProcess extends Disposable {
}

async attach(): Promise<void> {
// Something wrong happened if the disconnect runner is not canceled, this likely means
// multiple windows attempted to attach.
if (!await this._isOrphaned()) {
throw new Error(`Cannot attach to persistent process "${this._persistentProcessId}", it is already adopted`);
}
if (!this._disconnectRunner1.isScheduled() && !this._disconnectRunner2.isScheduled()) {
this._logService.warn(`Persistent process "${this._persistentProcessId}": Process had no disconnect runners but was an orphan`);
}
Expand Down
7 changes: 7 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/remotePty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Barrier } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { mark } from 'vs/base/common/performance';
import { URI } from 'vs/base/common/uri';
import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, IProcessReadyEvent, ITerminalLogService } from 'vs/platform/terminal/common/terminal';
Expand All @@ -32,6 +33,8 @@ export class RemotePty extends Disposable implements ITerminalChildProcess {

private readonly _onProcessData = this._register(new Emitter<string | IProcessDataEvent>());
readonly onProcessData = this._onProcessData.event;
private readonly _onProcessReplayComplete = this._register(new Emitter<void>());
readonly onProcessReplayComplete = this._onProcessReplayComplete.event;
private readonly _onProcessReady = this._register(new Emitter<IProcessReadyEvent>());
readonly onProcessReady = this._onProcessReady.event;
private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty<any>>());
Expand Down Expand Up @@ -176,6 +179,7 @@ export class RemotePty extends Disposable implements ITerminalChildProcess {
}

async handleReplay(e: IPtyHostProcessReplayEvent) {
mark(`code/terminal/willHandleReplay/${this.id}`);
try {
this._inReplay = true;
for (const innerEvent of e.events) {
Expand All @@ -197,6 +201,9 @@ export class RemotePty extends Disposable implements ITerminalChildProcess {

// remove size override
this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: undefined });

mark(`code/terminal/didHandleReplay/${this.id}`);
this._onProcessReplayComplete.fire();
}

handleOrphanQuestion() {
Expand Down
44 changes: 24 additions & 20 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,26 +155,29 @@ export interface ITerminalService extends ITerminalInstanceHost {
readonly instances: readonly ITerminalInstance[];
/** Gets detached terminal instances created via {@link createDetachedXterm}. */
readonly detachedXterms: Iterable<IXtermTerminal>;
configHelper: ITerminalConfigHelper;
isProcessSupportRegistered: boolean;
readonly configHelper: ITerminalConfigHelper;
readonly defaultLocation: TerminalLocation;

readonly isProcessSupportRegistered: boolean;
readonly connectionState: TerminalConnectionState;
readonly whenConnected: Promise<void>;
readonly defaultLocation: TerminalLocation;
/** The number of restored terminal groups on startup. */
readonly restoredGroupCount: number;

onDidChangeActiveGroup: Event<ITerminalGroup | undefined>;
onDidDisposeGroup: Event<ITerminalGroup>;
onDidCreateInstance: Event<ITerminalInstance>;
onDidReceiveProcessId: Event<ITerminalInstance>;
onDidChangeInstanceDimensions: Event<ITerminalInstance>;
onDidMaximumDimensionsChange: Event<ITerminalInstance>;
onDidRequestStartExtensionTerminal: Event<IStartExtensionTerminalRequest>;
onDidChangeInstanceTitle: Event<ITerminalInstance | undefined>;
onDidChangeInstanceIcon: Event<{ instance: ITerminalInstance; userInitiated: boolean }>;
onDidChangeInstanceColor: Event<{ instance: ITerminalInstance; userInitiated: boolean }>;
onDidChangeInstancePrimaryStatus: Event<ITerminalInstance>;
onDidInputInstanceData: Event<ITerminalInstance>;
onDidRegisterProcessSupport: Event<void>;
onDidChangeConnectionState: Event<void>;
readonly onDidChangeActiveGroup: Event<ITerminalGroup | undefined>;
readonly onDidDisposeGroup: Event<ITerminalGroup>;
readonly onDidCreateInstance: Event<ITerminalInstance>;
readonly onDidReceiveProcessId: Event<ITerminalInstance>;
readonly onDidChangeInstanceDimensions: Event<ITerminalInstance>;
readonly onDidMaximumDimensionsChange: Event<ITerminalInstance>;
readonly onDidRequestStartExtensionTerminal: Event<IStartExtensionTerminalRequest>;
readonly onDidChangeInstanceTitle: Event<ITerminalInstance | undefined>;
readonly onDidChangeInstanceIcon: Event<{ instance: ITerminalInstance; userInitiated: boolean }>;
readonly onDidChangeInstanceColor: Event<{ instance: ITerminalInstance; userInitiated: boolean }>;
readonly onDidChangeInstancePrimaryStatus: Event<ITerminalInstance>;
readonly onDidInputInstanceData: Event<ITerminalInstance>;
readonly onDidRegisterProcessSupport: Event<void>;
readonly onDidChangeConnectionState: Event<void>;

/**
* Creates a terminal.
Expand Down Expand Up @@ -228,9 +231,9 @@ export interface ITerminalService extends ITerminalInstanceHost {
safeDisposeTerminal(instance: ITerminalInstance): Promise<void>;

getDefaultInstanceHost(): ITerminalInstanceHost;
getInstanceHost(target: ITerminalLocationOptions | undefined): ITerminalInstanceHost;
getInstanceHost(target: ITerminalLocationOptions | undefined): Promise<ITerminalInstanceHost>;

resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined;
resolveLocation(location?: ITerminalLocationOptions): Promise<TerminalLocation | undefined>;
setNativeDelegate(nativeCalls: ITerminalServiceNativeDelegate): void;

getEditingTerminal(): ITerminalInstance | undefined;
Expand Down Expand Up @@ -286,7 +289,7 @@ export interface ISerializedTerminalEditorInput extends ITerminalEditorInputObje
export interface IDeserializedTerminalEditorInput extends ITerminalEditorInputObject {
}

export type ITerminalLocationOptions = TerminalLocation | TerminalEditorLocation | { parentTerminal: ITerminalInstance } | { splitActiveTerminal: boolean };
export type ITerminalLocationOptions = TerminalLocation | TerminalEditorLocation | { parentTerminal: Promise<ITerminalInstance> | ITerminalInstance } | { splitActiveTerminal: boolean };

export interface ICreateTerminalOptions {
/**
Expand Down Expand Up @@ -533,6 +536,7 @@ export interface ITerminalInstance {
onDisposed: Event<ITerminalInstance>;

onProcessIdReady: Event<ITerminalInstance>;
onProcessReplayComplete: Event<void>;
onRequestExtHostProcess: Event<ITerminalInstance>;
onDimensionsChanged: Event<void>;
onMaximumDimensionsChanged: Event<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ export function registerTerminalActions() {
const commandService = accessor.get(ICommandService);
const workspaceContextService = accessor.get(IWorkspaceContextService);
const options = convertOptionsOrProfileToOptions(optionsOrProfile);
const activeInstance = c.service.getInstanceHost(options?.location).activeInstance;
const activeInstance = (await c.service.getInstanceHost(options?.location)).activeInstance;
if (!activeInstance) {
return;
}
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
readonly onDisposed = this._onDisposed.event;
private readonly _onProcessIdReady = this._register(new Emitter<ITerminalInstance>());
readonly onProcessIdReady = this._onProcessIdReady.event;
private readonly _onProcessReplayComplete = this._register(new Emitter<void>());
readonly onProcessReplayComplete = this._onProcessReplayComplete.event;
private readonly _onTitleChanged = this._register(new Emitter<ITerminalInstance>());
readonly onTitleChanged = this._onTitleChanged.event;
private readonly _onIconChanged = this._register(new Emitter<{ instance: ITerminalInstance; userInitiated: boolean }>());
Expand Down Expand Up @@ -1395,6 +1397,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this._initialDataEvents?.push(ev.data);
this._onData.fire(ev.data);
});
processManager.onProcessReplayComplete(() => this._onProcessReplayComplete.fire());
processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e));
processManager.onPtyDisconnect(() => {
if (this.xterm) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
readonly onBeforeProcessData = this._onBeforeProcessData.event;
private readonly _onProcessData = this._register(new Emitter<IProcessDataEvent>());
readonly onProcessData = this._onProcessData.event;
private readonly _onProcessReplayComplete = this._register(new Emitter<void>());
readonly onProcessReplayComplete = this._onProcessReplayComplete.event;
private readonly _onDidChangeProperty = this._register(new Emitter<IProcessProperty<any>>());
readonly onDidChangeProperty = this._onDidChangeProperty.event;
private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter<IEnvironmentVariableInfo>());
Expand Down Expand Up @@ -375,10 +377,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
this._onDidChangeProperty.fire({ type, value });
})
];
if (newProcess.onProcessReplayComplete) {
this._processListeners.push(newProcess.onProcessReplayComplete(() => this._onProcessReplayComplete.fire()));
}
if (newProcess.onRestoreCommands) {
this._processListeners.push(newProcess.onRestoreCommands(e => {
this._onRestoreCommands.fire(e);
}));
this._processListeners.push(newProcess.onRestoreCommands(e => this._onRestoreCommands.fire(e)));
}
setTimeout(() => {
if (this.processState === ProcessState.Launching) {
Expand Down