From bfa57bdad5012d29e437e97747b4128d33e0bbf6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 16 Jun 2022 13:57:38 -0700 Subject: [PATCH] Restore old buffers if no additional interactions happened This makes terminals only restore their current session's buffer if there was some interaction by the user, similar to how the initial process doesn't get revived if no interaction occurred. Fixes #142855 --- src/vs/platform/terminal/node/ptyService.ts | 72 ++++++++++++++++----- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index ddadd6a069d3c..f7014d2a80c23 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -152,7 +152,8 @@ export class PtyService extends Disposable implements IPtyService { true, terminal.processDetails.workspaceId, terminal.processDetails.workspaceName, - true + 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 }); @@ -175,7 +176,8 @@ export class PtyService extends Disposable implements IPtyService { shouldPersist: boolean, workspaceId: string, workspaceName: string, - isReviving?: boolean + isReviving?: boolean, + rawReviveBuffer?: string ): Promise { if (shellLaunchConfig.attachPersistentProcess) { throw new Error('Attempt to create a process when attach object was provided'); @@ -188,7 +190,7 @@ export class PtyService extends Disposable implements IPtyService { executableEnv, options }; - const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, processLaunchOptions, unicodeVersion, this._reconnectConstants, this._logService, isReviving ? shellLaunchConfig.initialText : undefined, shellLaunchConfig.icon, shellLaunchConfig.color, shellLaunchConfig.name, shellLaunchConfig.fixedDimensions); + const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, processLaunchOptions, unicodeVersion, this._reconnectConstants, this._logService, isReviving ? shellLaunchConfig.initialText : undefined, rawReviveBuffer, shellLaunchConfig.icon, shellLaunchConfig.color, shellLaunchConfig.name, shellLaunchConfig.fixedDimensions); process.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); process.onProcessExit(event => { persistentProcess.dispose(); @@ -409,6 +411,15 @@ export class PtyService extends Disposable implements IPtyService { } } +const enum InteractionState { + /** The terminal has not been interacted with. */ + None = 0, + /** The terminal has only been interacted with by the replay mechanism. */ + ReplayOnly = 1, + /** The terminal has been directly interacted with this session. */ + Session = 2 +} + export class PersistentTerminalProcess extends Disposable { private readonly _bufferer: TerminalDataBufferer; @@ -417,7 +428,7 @@ export class PersistentTerminalProcess extends Disposable { private readonly _pendingCommands = new Map void; reject: (err: any) => void }>(); private _isStarted: boolean = false; - private _hasWrittenData: boolean = false; + private _interactionState: InteractionState = InteractionState.None; private _orphanQuestionBarrier: AutoOpenBarrier | null; private _orphanQuestionReplyTime: number; @@ -451,7 +462,7 @@ export class PersistentTerminalProcess extends Disposable { get pid(): number { return this._pid; } get shellLaunchConfig(): IShellLaunchConfig { return this._terminalProcess.shellLaunchConfig; } - get hasWrittenData(): boolean { return this._hasWrittenData; } + get hasWrittenData(): boolean { return this._interactionState !== InteractionState.None; } get title(): string { return this._title || this._terminalProcess.currentTitle; } get titleSource(): TitleEventSource { return this._titleSource; } get icon(): TerminalIcon | undefined { return this._icon; } @@ -459,13 +470,21 @@ export class PersistentTerminalProcess extends Disposable { get fixedDimensions(): IFixedTerminalDimensions | undefined { return this._fixedDimensions; } setTitle(title: string, titleSource: TitleEventSource): void { - this._hasWrittenData = true; + if (titleSource === TitleEventSource.Api) { + this._interactionState = InteractionState.Session; + this._serializer.freeRawReviveBuffer(); + } this._title = title; this._titleSource = titleSource; } setIcon(icon: TerminalIcon, color?: string): void { - this._hasWrittenData = true; + if (!this._icon || 'id' in icon && 'id' in this._icon && icon.id !== this._icon.id || + !this.color || color !== this._color) { + + this._serializer.freeRawReviveBuffer(); + this._interactionState = InteractionState.Session; + } this._icon = icon; this._color = color; } @@ -487,6 +506,7 @@ export class PersistentTerminalProcess extends Disposable { reconnectConstants: IReconnectConstants, private readonly _logService: ILogService, reviveBuffer: string | undefined, + rawReviveBuffer: string | undefined, private _icon?: TerminalIcon, private _color?: string, name?: string, @@ -504,6 +524,7 @@ export class PersistentTerminalProcess extends Disposable { reconnectConstants.scrollback, unicodeVersion, reviveBuffer, + rawReviveBuffer, this._logService ); this._fixedDimensions = fixedDimensions; @@ -559,7 +580,7 @@ export class PersistentTerminalProcess extends Disposable { } serializeNormalBuffer(): Promise { - return this._serializer.generateReplayEvent(true); + return this._serializer.generateReplayEvent(true, this._interactionState !== InteractionState.Session); } async refreshProperty(type: T): Promise { @@ -604,7 +625,8 @@ export class PersistentTerminalProcess extends Disposable { return this._terminalProcess.shutdown(immediate); } input(data: string): void { - this._hasWrittenData = true; + this._interactionState = InteractionState.Session; + this._serializer.freeRawReviveBuffer(); if (this._inReplay) { return; } @@ -652,7 +674,9 @@ export class PersistentTerminalProcess extends Disposable { } async triggerReplay(): Promise { - this._hasWrittenData = true; + if (this._interactionState === InteractionState.None) { + this._interactionState = InteractionState.ReplayOnly; + } const ev = await this._serializer.generateReplayEvent(); let dataLength = 0; for (const e of ev.events) { @@ -736,18 +760,24 @@ class XtermSerializer implements ITerminalSerializer { rows: number, scrollback: number, unicodeVersion: '6' | '11', - reviveBuffer: string | undefined, + reviveBufferWithRestoreMessage: string | undefined, + private _rawReviveBuffer: string | undefined, logService: ILogService ) { this._xterm = new XtermTerminal({ cols, rows, scrollback }); - if (reviveBuffer) { - this._xterm.writeln(reviveBuffer); + if (reviveBufferWithRestoreMessage) { + this._xterm.writeln(reviveBufferWithRestoreMessage); } this.setUnicodeVersion(unicodeVersion); this._shellIntegrationAddon = new ShellIntegrationAddon(true, undefined, logService); this._xterm.loadAddon(this._shellIntegrationAddon); } + freeRawReviveBuffer(): void { + // Free the memory if the terminal if it will need to be re-serialized + this._rawReviveBuffer = undefined; + } + handleData(data: string): void { this._xterm.write(data); } @@ -756,15 +786,22 @@ class XtermSerializer implements ITerminalSerializer { this._xterm.resize(cols, rows); } - async generateReplayEvent(normalBufferOnly?: boolean): Promise { + async generateReplayEvent(normalBufferOnly?: boolean, restoreToLastReviveBuffer?: boolean): Promise { const serialize = new (await this._getSerializeConstructor()); this._xterm.loadAddon(serialize); - const options: ISerializeOptions = { scrollback: this._xterm.getOption('scrollback') }; + const options: ISerializeOptions = { + scrollback: this._xterm.getOption('scrollback') + }; if (normalBufferOnly) { options.excludeAltBuffer = true; options.excludeModes = true; } - const serialized = serialize.serialize(options); + let serialized: string; + if (restoreToLastReviveBuffer && this._rawReviveBuffer) { + serialized = this._rawReviveBuffer; + } else { + serialized = serialize.serialize(options); + } return { events: [ { @@ -831,7 +868,8 @@ function printTime(ms: number): string { export interface ITerminalSerializer { handleData(data: string): void; + freeRawReviveBuffer(): void; handleResize(cols: number, rows: number): void; - generateReplayEvent(normalBufferOnly?: boolean): Promise; + generateReplayEvent(normalBufferOnly?: boolean, restoreToLastReviveBuffer?: boolean): Promise; setUnicodeVersion?(version: '6' | '11'): void; }