diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index c5ffacc287614..5ea96bc9d1373 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -240,6 +240,7 @@ export interface IPtyService { reduceConnectionGraceTime(): Promise; requestDetachInstance(workspaceId: string, instanceId: number): Promise; acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise; + persistTerminalState(): Promise; } export interface IRequestResolveVariablesEvent { diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index b2d12a0317337..811f93624f4e7 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -266,6 +266,10 @@ export class PtyHostService extends Disposable implements IPtyService { return this._proxy.acceptDetachInstanceReply(requestId, persistentProcessId); } + async persistTerminalState(): Promise { + return this._proxy.persistTerminalState(); + } + async restartPtyHost(): Promise { /* __GDPR__ "ptyHost/restart" : {} @@ -277,9 +281,7 @@ export class PtyHostService extends Disposable implements IPtyService { } private _disposePtyHost(): void { - if (this._proxy.shutdownAll) { - this._proxy.shutdownAll(); - } + this._proxy.shutdownAll?.(); this._client.dispose(); } diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 9d4e1f542b5ab..0ef09a12142a8 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -22,6 +22,8 @@ import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent import { ITerminalSerializer, TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess'; +import { Promises as pfs } from 'vs/base/node/pfs'; +import { join } from 'path'; type WorkspaceId = string; @@ -97,6 +99,21 @@ export class PtyService extends Disposable implements IPtyService { this._detachInstanceRequestStore.acceptReply(requestId, processDetails); } + async persistTerminalState(): Promise { + let processes: any[] = []; + for (const [persistentProcessId, persistentProcess] of this._ptys.entries()) { + processes.push({ + id: persistentProcessId, + // TODO: Serialize in parallel + buffer: await (persistentProcess as any)._serializer.generateReplayEvent() + }); + } + const state = { + processes + }; + pfs.writeFile(join(process.env.HOME!, 'testbuffer2'), JSON.stringify(state)); + } + async shutdownAll(): Promise { this.dispose(); } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 0ae07e4cfc7fc..5041fdaeb479f 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -172,6 +172,10 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal return this._remoteTerminalChannel.acceptDetachInstanceReply(requestId, persistentProcessId); } + async persistTerminalState(): Promise { + throw new Error('NYI'); // TODO: Implement + } + async createProcess(shellLaunchConfig: IShellLaunchConfig, configuration: ICompleteTerminalConfiguration, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, unicodeVersion: '6' | '11', shouldPersist: boolean): Promise { if (!this._remoteTerminalChannel) { throw new Error(`Cannot create remote terminal when there is no remote!`); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 86ca9b58c4155..fa4b979af1b5e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -529,12 +529,21 @@ export class TerminalService implements ITerminalService { return this._defaultProfileName; } - private _onBeforeShutdown(reason: ShutdownReason): boolean | Promise { + private async _onBeforeShutdown(reason: ShutdownReason): Promise { if (this.instances.length === 0) { // No terminal instances, don't veto return false; } + // TODO: Fine tune which reasons are supported - what is LOAD? + console.log('persist buffer, reason: ' + reason); + if (reason === ShutdownReason.CLOSE) { + // TODO: persist buffer to disk + // TODO: This is called once per workspace? + // TODO: Either do this or confirm dialog? + await this._localTerminalService?.persistTerminalState(); + } + const shouldPersistTerminals = this._configHelper.config.enablePersistentSessions && reason === ShutdownReason.RELOAD; if (!shouldPersistTerminals) { const hasDirtyInstances = ( @@ -542,7 +551,7 @@ export class TerminalService implements ITerminalService { (this.configHelper.config.confirmOnExit === 'hasChildProcesses' && this.instances.some(e => e.hasChildProcesses)) ); if (hasDirtyInstances) { - return this._onBeforeShutdownAsync(); + return this._onBeforeShutdownAsync(reason); } } @@ -551,12 +560,13 @@ export class TerminalService implements ITerminalService { return false; } - private async _onBeforeShutdownAsync(): Promise { + private async _onBeforeShutdownAsync(reason: ShutdownReason): Promise { // veto if configured to show confirmation and the user chose not to exit const veto = await this._showTerminalCloseConfirmation(); if (!veto) { this._isShuttingDown = true; } + return veto; } @@ -564,13 +574,16 @@ export class TerminalService implements ITerminalService { // Don't touch processes if the shutdown was a result of reload as they will be reattached const shouldPersistTerminals = this._configHelper.config.enablePersistentSessions && e.reason === ShutdownReason.RELOAD; if (shouldPersistTerminals) { - this.instances.forEach(instance => instance.detachFromProcess()); + for (const instance of this.instances) { + instance.detachFromProcess(); + } return; } // Force dispose of all terminal instances - this.instances.forEach(instance => instance.dispose(true)); - + for (const instance of this.instances) { + instance.dispose(); + } this._localTerminalService?.setTerminalLayoutInfo(undefined); } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 0d894c22f7475..b35cc9a961ac7 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -97,6 +97,10 @@ export interface IOffProcessTerminalService { reduceConnectionGraceTime(): Promise; requestDetachInstance(workspaceId: string, instanceId: number): Promise; acceptDetachInstanceReply(requestId: number, persistentProcessId?: number): Promise; + /** + * Persists a terminal state to disk, this will a + */ + persistTerminalState(): Promise; } export const ILocalTerminalService = createDecorator('localTerminalService'); diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts index fc155cf9aa0f8..0831608305f02 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts @@ -138,6 +138,10 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe return this._localPtyService.acceptDetachInstanceReply(requestId, persistentProcessId); } + async persistTerminalState(): Promise { + return this._localPtyService.persistTerminalState(); + } + async updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise { await this._localPtyService.updateTitle(id, title, titleSource); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index d60ca25a758ca..5b321061e121a 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1691,6 +1691,7 @@ export class TestLocalTerminalService implements ILocalTerminalService { updateIcon(id: number, icon: URI | { light: URI; dark: URI } | { id: string, color?: { id: string } }, color?: string): Promise { throw new Error('Method not implemented.'); } requestDetachInstance(workspaceId: string, instanceId: number): Promise { throw new Error('Method not implemented.'); } acceptDetachInstanceReply(requestId: number, persistentProcessId: number): Promise { throw new Error('Method not implemented.'); } + persistTerminalState(): Promise { throw new Error('Method not implemented.'); } } class TestTerminalChildProcess implements ITerminalChildProcess {