diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 5e7a544291da8..e25b05b3ddae1 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -19,6 +19,8 @@ import { assertNoRpc } from '../utils'; extensionContext = (global as any).testExtensionContext; const config = workspace.getConfiguration('terminal.integrated'); + // Disable conpty in integration tests because of https://github.com/microsoft/vscode/issues/76548 + await config.update('windowsEnableConpty', false, ConfigurationTarget.Global); // Disable exit alerts as tests may trigger then and we're not testing the notifications await config.update('showExitAlert', false, ConfigurationTarget.Global); // Canvas may cause problems when running in a container @@ -637,10 +639,7 @@ import { assertNoRpc } from '../utils'; '~c2~c1' ]; disposables.push(window.onDidWriteTerminalData(e => { - try { - equal(terminal, e.terminal); - } catch (e) { - done(e); + if (terminal !== e.terminal) { return; } // Multiple expected could show up in the same data event @@ -683,10 +682,7 @@ import { assertNoRpc } from '../utils'; '~c2~' ]; disposables.push(window.onDidWriteTerminalData(e => { - try { - equal(terminal, e.terminal); - } catch (e) { - done(e); + if (terminal !== e.terminal) { return; } // Multiple expected could show up in the same data event @@ -728,10 +724,7 @@ import { assertNoRpc } from '../utils'; '~b1~' ]; disposables.push(window.onDidWriteTerminalData(e => { - try { - equal(terminal, e.terminal); - } catch (e) { - done(e); + if (terminal !== e.terminal) { return; } // Multiple expected could show up in the same data event @@ -770,10 +763,7 @@ import { assertNoRpc } from '../utils'; '~b2~' ]; disposables.push(window.onDidWriteTerminalData(e => { - try { - equal(terminal, e.terminal); - } catch (e) { - done(e); + if (terminal !== e.terminal) { return; } // Multiple expected could show up in the same data event diff --git a/package.json b/package.json index fd614afd9b513..4aed66e7efa3f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.54.0", - "distro": "15d06a28bcd9dc439ef80df6851d875624b6ff0f", + "distro": "79bf0c38866670404869fbc506d1622e551d27b6", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index a2474fe84b4d9..28935409f0225 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -367,6 +367,25 @@ export class Barrier { } } +/** + * A barrier that is initially closed and then becomes opened permanently after a certain period of + * time or when open is called explicitly + */ +export class AutoOpenBarrier extends Barrier { + + private readonly _timeout: any; + + constructor(autoOpenTimeMs: number) { + super(); + this._timeout = setTimeout(() => this.open(), autoOpenTimeMs); + } + + open(): void { + clearTimeout(this._timeout); + super.open(); + } +} + export function timeout(millis: number): CancelablePromise; export function timeout(millis: number, token: CancellationToken): Promise; export function timeout(millis: number, token?: CancellationToken): CancelablePromise | Promise { diff --git a/src/vs/platform/terminal/common/environmentVariable.ts b/src/vs/platform/terminal/common/environmentVariable.ts new file mode 100644 index 0000000000000..455925cfb43c1 --- /dev/null +++ b/src/vs/platform/terminal/common/environmentVariable.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export enum EnvironmentVariableMutatorType { + Replace = 1, + Append = 2, + Prepend = 3 +} +export interface IEnvironmentVariableMutator { + readonly value: string; + readonly type: EnvironmentVariableMutatorType; +} +/** [variable, mutator] */ +export type ISerializableEnvironmentVariableCollection = [string, IEnvironmentVariableMutator][]; diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 3f54a3daf4983..fb4f5d1141613 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -6,6 +6,45 @@ import { Event } from 'vs/base/common/event'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; + +export interface IRawTerminalInstanceLayoutInfo { + relativeSize: number; + terminal: T; +} +export type ITerminalInstanceLayoutInfoById = IRawTerminalInstanceLayoutInfo; +export type ITerminalInstanceLayoutInfo = IRawTerminalInstanceLayoutInfo; + +export interface IRawTerminalTabLayoutInfo { + isActive: boolean; + activePersistentTerminalId: number | undefined; + terminals: IRawTerminalInstanceLayoutInfo[]; +} + +export type ITerminalTabLayoutInfoById = IRawTerminalTabLayoutInfo; +export type ITerminalTabLayoutInfo = IRawTerminalTabLayoutInfo; + +export interface IRawTerminalsLayoutInfo { + tabs: IRawTerminalTabLayoutInfo[]; +} + +export interface IPtyHostAttachTarget { + id: number; + pid: number; + title: string; + cwd: string; + workspaceId: string; + workspaceName: string; + isOrphan: boolean; +} + +export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo; +export type ITerminalsLayoutInfoById = IRawTerminalsLayoutInfo; + +export interface IRawTerminalInstanceLayoutInfo { + relativeSize: number; + terminal: T; +} export enum TerminalIpcChannels { /** @@ -39,6 +78,10 @@ export interface IPtyService { readonly onProcessTitleChanged: Event<{ id: number, event: string }>; readonly onProcessOverrideDimensions: Event<{ id: number, event: ITerminalDimensionsOverride | undefined }>; readonly onProcessResolvedShellLaunchConfig: Event<{ id: number, event: IShellLaunchConfig }>; + readonly onProcessReplay: Event<{ id: number, event: IPtyHostProcessReplayEvent }>; + + restartPtyHost?(): Promise; + shutdownAll?(): Promise; createProcess( shellLaunchConfig: IShellLaunchConfig, @@ -47,28 +90,23 @@ export interface IPtyService { rows: number, env: IProcessEnvironment, executableEnv: IProcessEnvironment, - windowsEnableConpty: boolean + windowsEnableConpty: boolean, + workspaceId: string, + workspaceName: string ): Promise; + attachToProcess(id: number): Promise; - shutdownAll?(): Promise; - - start(id: number): Promise; - + start(id: number): Promise; shutdown(id: number, immediate: boolean): Promise; - input(id: number, data: string): Promise; - resize(id: number, cols: number, rows: number): Promise; - - acknowledgeDataEvent(id: number, charCount: number): Promise; - getInitialCwd(id: number): Promise; - getCwd(id: number): Promise; - getLatency(id: number): Promise; + acknowledgeDataEvent(id: number, charCount: number): Promise; - restartPtyHost?(): Promise; + setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void; + getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise; } export enum HeartbeatConstants { @@ -92,7 +130,7 @@ export enum HeartbeatConstants { * process. This short circuits the standard wait timeouts to tell the user sooner and only * create process is handled to avoid additional perf overhead. */ - CreateProcessTimeout = 2000 + CreateProcessTimeout = 5000 } export interface IHeartbeatService { @@ -157,9 +195,9 @@ export interface IShellLaunchConfig { extHostTerminalId?: string; /** - * This is a terminal that attaches to an already running remote terminal. + * This is a terminal that attaches to an already running terminal. */ - remoteAttach?: { id: number; pid: number; title: string; cwd: string; }; + attachPersistentTerminal?: { id: number; pid: number; title: string; cwd: string; }; /** * Whether the terminal process environment should be exactly as provided in @@ -218,7 +256,7 @@ export interface ITerminalChildProcess { * @returns undefined when the process was successfully started, otherwise an object containing * information on what went wrong. */ - start(): Promise; + start(): Promise; /** * Shutdown the terminal process. @@ -243,6 +281,17 @@ export interface ITerminalChildProcess { getLatency(): Promise; } +export const enum LocalReconnectConstants { + /** + * If there is no reconnection within this time-frame, consider the connection permanently closed... + */ + ReconnectionGraceTime = 5000, // 5 seconds + /** + * Maximal grace time between the first and the last reconnection... + */ + ReconnectionShortGraceTime = 1000, // 1 second +} + export const enum FlowControlConstants { /** * The number of _unacknowledged_ chars to have been sent before the pty is paused in order for diff --git a/src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts b/src/vs/platform/terminal/common/terminalDataBuffering.ts similarity index 100% rename from src/vs/workbench/contrib/terminal/common/terminalDataBuffering.ts rename to src/vs/platform/terminal/common/terminalDataBuffering.ts diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts new file mode 100644 index 0000000000000..c1907921e7458 --- /dev/null +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { UriComponents } from 'vs/base/common/uri'; +import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; + +export interface IShellLaunchConfigDto { + name?: string; + executable?: string; + args?: string[] | string; + cwd?: string | UriComponents; + env?: { [key: string]: string | null; }; + hideFromUser?: boolean; +} + +export interface ISingleTerminalConfiguration { + userValue: T | undefined; + value: T | undefined; + defaultValue: T | undefined; +} + +export interface ICompleteTerminalConfiguration { + 'terminal.integrated.automationShell.windows': ISingleTerminalConfiguration; + 'terminal.integrated.automationShell.osx': ISingleTerminalConfiguration; + 'terminal.integrated.automationShell.linux': ISingleTerminalConfiguration; + 'terminal.integrated.shell.windows': ISingleTerminalConfiguration; + 'terminal.integrated.shell.osx': ISingleTerminalConfiguration; + 'terminal.integrated.shell.linux': ISingleTerminalConfiguration; + 'terminal.integrated.shellArgs.windows': ISingleTerminalConfiguration; + 'terminal.integrated.shellArgs.osx': ISingleTerminalConfiguration; + 'terminal.integrated.shellArgs.linux': ISingleTerminalConfiguration; + 'terminal.integrated.env.windows': ISingleTerminalConfiguration; + 'terminal.integrated.env.osx': ISingleTerminalConfiguration; + 'terminal.integrated.env.linux': ISingleTerminalConfiguration; + 'terminal.integrated.inheritEnv': boolean; + 'terminal.integrated.cwd': string; + 'terminal.integrated.detectLocale': 'auto' | 'off' | 'on'; + 'terminal.flowControl': boolean; +} + +export type ITerminalEnvironmentVariableCollections = [string, ISerializableEnvironmentVariableCollection][]; + +export interface IWorkspaceFolderData { + uri: UriComponents; + name: string; + index: number; +} + +export interface ISetTerminalLayoutInfoArgs { + workspaceId: string; + tabs: ITerminalTabLayoutInfoById[]; +} + +export interface IGetTerminalLayoutInfoArgs { + workspaceId: string; +} + +export interface IPtyHostDescriptionDto { + id: number; + pid: number; + title: string; + cwd: string; + workspaceId: string; + workspaceName: string; + isOrphan: boolean; +} + +export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; + +export interface ReplayEntry { cols: number; rows: number; data: string; } +export interface IPtyHostProcessReplayEvent { + events: ReplayEntry[]; +} diff --git a/src/vs/platform/terminal/common/terminalRecorder.ts b/src/vs/platform/terminal/common/terminalRecorder.ts new file mode 100644 index 0000000000000..6341bcc17a5d1 --- /dev/null +++ b/src/vs/platform/terminal/common/terminalRecorder.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; + +const MAX_RECORDER_DATA_SIZE = 1024 * 1024; // 1MB + +interface RecorderEntry { + cols: number; + rows: number; + data: string[]; +} + +export interface ReplayEntry { cols: number; rows: number; data: string; } + +export interface IRemoteTerminalProcessReplayEvent { + events: ReplayEntry[]; +} + +export class TerminalRecorder { + + private _entries: RecorderEntry[]; + private _totalDataLength: number; + + constructor(cols: number, rows: number) { + this._entries = [{ cols, rows, data: [] }]; + this._totalDataLength = 0; + } + + public recordResize(cols: number, rows: number): void { + if (this._entries.length > 0) { + const lastEntry = this._entries[this._entries.length - 1]; + if (lastEntry.data.length === 0) { + // last entry is just a resize, so just remove it + this._entries.pop(); + } + } + + if (this._entries.length > 0) { + const lastEntry = this._entries[this._entries.length - 1]; + if (lastEntry.cols === cols && lastEntry.rows === rows) { + // nothing changed + return; + } + if (lastEntry.cols === 0 && lastEntry.rows === 0) { + // we finally received a good size! + lastEntry.cols = cols; + lastEntry.rows = rows; + return; + } + } + + this._entries.push({ cols, rows, data: [] }); + } + + public recordData(data: string): void { + const lastEntry = this._entries[this._entries.length - 1]; + lastEntry.data.push(data); + + this._totalDataLength += data.length; + while (this._totalDataLength > MAX_RECORDER_DATA_SIZE) { + const firstEntry = this._entries[0]; + const remainingToDelete = this._totalDataLength - MAX_RECORDER_DATA_SIZE; + if (remainingToDelete >= firstEntry.data[0].length) { + // the first data piece must be deleted + this._totalDataLength -= firstEntry.data[0].length; + firstEntry.data.shift(); + if (firstEntry.data.length === 0) { + // the first entry must be deleted + this._entries.shift(); + } + } else { + // the first data piece must be partially deleted + firstEntry.data[0] = firstEntry.data[0].substr(remainingToDelete); + this._totalDataLength -= remainingToDelete; + } + } + } + + public generateReplayEvent(): IPtyHostProcessReplayEvent { + // normalize entries to one element per data array + this._entries.forEach((entry) => { + if (entry.data.length > 0) { + entry.data = [entry.data.join('')]; + } + }); + return { + events: this._entries.map(entry => ({ cols: entry.cols, rows: entry.rows, data: entry.data[0] ?? '' })) + }; + } +} diff --git a/src/vs/platform/terminal/electron-browser/localPtyService.ts b/src/vs/platform/terminal/electron-browser/localPtyService.ts index 837e4ccaaf468..a0f231e8dab60 100644 --- a/src/vs/platform/terminal/electron-browser/localPtyService.ts +++ b/src/vs/platform/terminal/electron-browser/localPtyService.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants } from 'vs/platform/terminal/common/terminal'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants } from 'vs/platform/terminal/common/terminal'; import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { FileAccess } from 'vs/base/common/network'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { Emitter } from 'vs/base/common/event'; import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; +import { IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; enum Constants { MaxRestarts = 5 @@ -43,6 +44,8 @@ export class LocalPtyService extends Disposable implements IPtyService { readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string } }>()); readonly onProcessReady = this._onProcessReady.event; + private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>()); + readonly onProcessReplay = this._onProcessReplay.event; private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>()); readonly onProcessTitleChanged = this._onProcessTitleChanged.event; private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>()); @@ -55,6 +58,8 @@ export class LocalPtyService extends Disposable implements IPtyService { ) { super(); + this._register(toDisposable(() => this._disposePtyHost())); + [this._client, this._proxy] = this._startPtyHost(); } @@ -77,11 +82,6 @@ export class LocalPtyService extends Disposable implements IPtyService { heartbeatService.onBeat(() => this._handleHeartbeat()); // Handle exit - this._register({ - dispose: () => { - this._disposePtyHost(); - } - }); this._register(client.onDidProcessExit(e => { this._onPtyHostExit.fire(e.code); if (!this._isDisposed) { @@ -109,6 +109,8 @@ export class LocalPtyService extends Disposable implements IPtyService { this._register(proxy.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e))); this._register(proxy.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e))); this._register(proxy.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e))); + this._register(proxy.onProcessReplay(e => this._onProcessReplay.fire(e))); + return [client, proxy]; } @@ -117,13 +119,18 @@ export class LocalPtyService extends Disposable implements IPtyService { super.dispose(); } - async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean): Promise { + async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean, workspaceId: string, workspaceName: string): Promise { const timeout = setTimeout(() => this._handleUnresponsiveCreateProcess(), HeartbeatConstants.CreateProcessTimeout); - const result = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty); + const result = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, workspaceId, workspaceName); clearTimeout(timeout); return result; } - start(id: number): Promise { + + attachToProcess(id: number): Promise { + return this._proxy.attachToProcess(id); + } + + start(id: number): Promise { return this._proxy.start(id); } shutdown(id: number, immediate: boolean): Promise { @@ -147,6 +154,12 @@ export class LocalPtyService extends Disposable implements IPtyService { getLatency(id: number): Promise { return this._proxy.getLatency(id); } + setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void { + return this._proxy.setTerminalLayoutInfo(args); + } + getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise { + return this._proxy.getTerminalLayoutInfo(args); + } async restartPtyHost(): Promise { this._disposePtyHost(); diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 1c260f3f56b2a..98982776e674b 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -3,26 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment } from 'vs/base/common/platform'; -import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; -import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess'; +import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; +import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; +import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess'; +import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IPtyHostDescriptionDto, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; import { ILogService } from 'vs/platform/log/common/log'; +import { createRandomIPCHandle } from 'vs/base/parts/ipc/node/ipc.net'; // TODO: On disconnect/restart, this will overwrite the older terminals let currentPtyId = 0; +type WorkspaceId = string; + export class PtyService extends Disposable implements IPtyService { declare readonly _serviceBrand: undefined; - private readonly _ptys: Map = new Map(); + private readonly _ptys: Map = new Map(); + private readonly _workspaceLayoutInfos = new Map(); private readonly _onHeartbeat = this._register(new Emitter()); readonly onHeartbeat = this._onHeartbeat.event; private readonly _onProcessData = this._register(new Emitter<{ id: number, event: IProcessDataEvent | string }>()); readonly onProcessData = this._onProcessData.event; + private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>()); + readonly onProcessReplay = this._onProcessReplay.event; private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>()); readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string } }>()); @@ -33,74 +42,136 @@ export class PtyService extends Disposable implements IPtyService { readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>()); readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event; - constructor( private readonly _logService: ILogService ) { super(); + + this._register(toDisposable(() => { + for (const pty of this._ptys.values()) { + pty.shutdown(true); + } + this._ptys.clear(); + })); } - dispose() { - for (const pty of this._ptys.values()) { - pty.shutdown(true); - } - this._ptys.clear(); + async shutdownAll(): Promise { + this.dispose(); } - async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean): Promise { + async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean, workspaceId: string, workspaceName: string): Promise { + if (shellLaunchConfig.attachPersistentTerminal) { + throw new Error('Attempt to create a process when attach object was provided'); + } const id = ++currentPtyId; const process = new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, this._logService); process.onProcessData(event => this._onProcessData.fire({ id, event })); process.onProcessExit(event => this._onProcessExit.fire({ id, event })); - process.onProcessReady(event => this._onProcessReady.fire({ id, event })); - process.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event })); if (process.onProcessOverrideDimensions) { process.onProcessOverrideDimensions(event => this._onProcessOverrideDimensions.fire({ id, event })); } if (process.onProcessResolvedShellLaunchConfig) { process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event })); } - this._ptys.set(id, process); + const ipcHandlePath = createRandomIPCHandle(); + env.VSCODE_IPC_HOOK_CLI = ipcHandlePath; + const persistentTerminalProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, true, cols, rows, ipcHandlePath, this._logService); + process.onProcessExit(() => { + persistentTerminalProcess.dispose(); + this._ptys.delete(id); + }); + persistentTerminalProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event })); + persistentTerminalProcess.onProcessReady(event => this._onProcessReady.fire({ id, event })); + persistentTerminalProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event })); + this._ptys.set(id, persistentTerminalProcess); return id; } - async shutdownAll(): Promise { - this.dispose(); + async attachToProcess(id: number): Promise { + this._throwIfNoPty(id); + this._logService.trace(`Persistent terminal "${id}": Attach`); } - async start(id: number): Promise { + async start(id: number): Promise { return this._throwIfNoPty(id).start(); } - async shutdown(id: number, immediate: boolean): Promise { return this._throwIfNoPty(id).shutdown(immediate); } - async input(id: number, data: string): Promise { return this._throwIfNoPty(id).input(data); } - async resize(id: number, cols: number, rows: number): Promise { return this._throwIfNoPty(id).resize(cols, rows); } - + async getInitialCwd(id: number): Promise { + return this._throwIfNoPty(id).getInitialCwd(); + } + async getCwd(id: number): Promise { + return this._throwIfNoPty(id).getCwd(); + } async acknowledgeDataEvent(id: number, charCount: number): Promise { return this._throwIfNoPty(id).acknowledgeDataEvent(charCount); } + async getLatency(id: number): Promise { + return 0; + } + async triggerReplay(id: number): Promise { + return this._throwIfNoPty(id).triggerReplay(); + } - async getInitialCwd(id: number): Promise { - return this._throwIfNoPty(id).getInitialCwd(); + async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { + this._workspaceLayoutInfos.set(args.workspaceId, args); } - async getCwd(id: number): Promise { - return this._throwIfNoPty(id).getCwd(); + async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise { + if (args) { + const layout = this._workspaceLayoutInfos.get(args.workspaceId); + if (layout) { + const expandedTabs = await Promise.all(layout.tabs.map(async tab => this._expandTerminalTab(tab))); + const filtered = expandedTabs.filter(t => t.terminals.length > 0); + return { + tabs: filtered + }; + } + + } + return undefined; } - async getLatency(id: number): Promise { - return this._throwIfNoPty(id).getLatency(); + private async _expandTerminalTab(tab: ITerminalTabLayoutInfoById): Promise { + const expandedTerminals = (await Promise.all(tab.terminals.map(t => this._expandTerminalInstance(t)))); + const filtered = expandedTerminals.filter(term => term.terminal !== null) as IRawTerminalInstanceLayoutInfo[]; + return { + isActive: tab.isActive, + activePersistentTerminalId: tab.activePersistentTerminalId, + terminals: filtered + }; } - private _throwIfNoPty(id: number): ITerminalChildProcess { + private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise> { + const persistentTerminalProcess = this._throwIfNoPty(t.terminal); + const termDto = persistentTerminalProcess && await this._terminalToDto(t.terminal, persistentTerminalProcess); + return { + terminal: termDto ?? null, + relativeSize: t.relativeSize + }; + } + + private async _terminalToDto(id: number, persistentTerminalProcess: PersistentTerminalProcess): Promise { + const [cwd, isOrphan] = await Promise.all([persistentTerminalProcess.getCwd(), persistentTerminalProcess.isOrphaned()]); + return { + id, + title: persistentTerminalProcess.title, + pid: persistentTerminalProcess.pid, + workspaceId: persistentTerminalProcess.workspaceId, + workspaceName: persistentTerminalProcess.workspaceName, + cwd, + isOrphan + }; + } + + private _throwIfNoPty(id: number): PersistentTerminalProcess { const pty = this._ptys.get(id); if (!pty) { throw new Error(`Could not find pty with id "${id}"`); @@ -108,3 +179,221 @@ export class PtyService extends Disposable implements IPtyService { return pty; } } + +export class PersistentTerminalProcess extends Disposable { + + // private readonly _bufferer: TerminalDataBufferer; + + private readonly _pendingCommands = new Map void; reject: (err: any) => void; }>(); + + private readonly _recorder: TerminalRecorder; + private _isStarted: boolean = false; + + private _orphanQuestionBarrier: AutoOpenBarrier | null; + private _orphanQuestionReplyTime: number; + private _orphanRequestQueue = new Queue(); + private _disconnectRunner1: RunOnceScheduler; + private _disconnectRunner2: RunOnceScheduler; + + private readonly _onProcessReplay = this._register(new Emitter()); + readonly onProcessReplay = this._onProcessReplay.event; + private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); + readonly onProcessReady = this._onProcessReady.event; + private readonly _onProcessTitleChanged = this._register(new Emitter()); + readonly onProcessTitleChanged = this._onProcessTitleChanged.event; + private readonly _onProcessOverrideDimensions = this._register(new Emitter()); + readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; + private readonly _onProcessData = this._register(new Emitter()); + readonly onProcessData = this._onProcessData.event; + + private _inReplay = false; + + private _pid = -1; + private _cwd = ''; + + get pid(): number { return this._pid; } + get title(): string { return this._terminalProcess.currentTitle; } + + constructor( + private _persistentTerminalId: number, + private readonly _terminalProcess: TerminalProcess, + public readonly workspaceId: string, + public readonly workspaceName: string, + public readonly shouldPersistTerminal: boolean, + cols: number, rows: number, + // TODO: This needs to get used? + ipcHandlePath: string, + private readonly _logService: ILogService + ) { + super(); + this._recorder = new TerminalRecorder(cols, rows); + this._orphanQuestionBarrier = null; + this._orphanQuestionReplyTime = 0; + this._disconnectRunner1 = this._register(new RunOnceScheduler(() => { + this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`); + this.shutdown(true); + }, LocalReconnectConstants.ReconnectionGraceTime)); + this._disconnectRunner2 = this._register(new RunOnceScheduler(() => { + this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`); + this.shutdown(true); + }, LocalReconnectConstants.ReconnectionShortGraceTime)); + + // TODO: Bring back bufferer + // this._bufferer = new TerminalDataBufferer((id, data) => { + // const ev: IPtyHostProcessDataEvent = { + // type: 'data', + // data: data + // }; + // this._events.fire(ev); + // }); + + this._register(this._terminalProcess.onProcessReady(e => { + this._pid = e.pid; + this._cwd = e.cwd; + this._onProcessReady.fire(e); + })); + this._register(this._terminalProcess.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e))); + + // Buffer data events to reduce the amount of messages going to the renderer + // this._register(this._bufferer.startBuffering(this._persistentTerminalId, this._terminalProcess.onProcessData)); + this._register(this._terminalProcess.onProcessData(e => this._recorder.recordData(e))); + this._register(this._terminalProcess.onProcessExit(exitCode => { + // this._bufferer.stopBuffering(this._persistentTerminalId); + })); + } + + async start(): Promise { + let result; + if (!this._isStarted) { + result = await this._terminalProcess.start(); + if (result) { + // it's a terminal launch error + return result; + } + this._isStarted = true; + } else { + this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd }); + this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle); + this.triggerReplay(); + } + return { persistentTerminalId: this._persistentTerminalId }; + } + shutdown(immediate: boolean): void { + return this._terminalProcess.shutdown(immediate); + } + input(data: string): void { + if (this._inReplay) { + return; + } + return this._terminalProcess.input(data); + } + resize(cols: number, rows: number): void { + if (this._inReplay) { + return; + } + this._recorder.recordResize(cols, rows); + return this._terminalProcess.resize(cols, rows); + } + acknowledgeDataEvent(charCount: number): void { + if (this._inReplay) { + return; + } + return this._terminalProcess.acknowledgeDataEvent(charCount); + } + getInitialCwd(): Promise { + return this._terminalProcess.getInitialCwd(); + } + getCwd(): Promise { + return this._terminalProcess.getCwd(); + } + getLatency(): Promise { + return this._terminalProcess.getLatency(); + } + + triggerReplay(): void { + const ev = this._recorder.generateReplayEvent(); + let dataLength = 0; + for (const e of ev.events) { + dataLength += e.data.length; + } + + this._logService.info(`Persistent terminal "${this._persistentTerminalId}": Replaying ${dataLength} chars and ${ev.events.length} size events`); + this._onProcessReplay.fire(ev); + this._terminalProcess.clearUnacknowledgedChars(); + } + + sendCommandResult(reqId: number, isError: boolean, serializedPayload: any): void { + const data = this._pendingCommands.get(reqId); + if (!data) { + return; + } + this._pendingCommands.delete(reqId); + } + + async orphanQuestionReply(): Promise { + this._orphanQuestionReplyTime = Date.now(); + if (this._orphanQuestionBarrier) { + const barrier = this._orphanQuestionBarrier; + this._orphanQuestionBarrier = null; + barrier.open(); + } + } + + reduceGraceTime(): void { + if (this._disconnectRunner2.isScheduled()) { + // we are disconnected and already running the short reconnection timer + return; + } + if (this._disconnectRunner1.isScheduled()) { + // we are disconnected and running the long reconnection timer + this._disconnectRunner2.schedule(); + } + } + + async isOrphaned(): Promise { + return await this._orphanRequestQueue.queue(async () => this._isOrphaned()); + } + + private async _isOrphaned(): Promise { + if (this._disconnectRunner1.isScheduled() || this._disconnectRunner2.isScheduled()) { + return true; + } + + if (!this._orphanQuestionBarrier) { + // the barrier opens after 4 seconds with or without a reply + this._orphanQuestionBarrier = new AutoOpenBarrier(4000); + this._orphanQuestionReplyTime = 0; + // TODO: Fire? + // const ev: IPtyHostProcessOrphanQuestionEvent = { + // type: 'orphan?' + // }; + // this._events.fire(ev); + } + + await this._orphanQuestionBarrier.wait(); + return (Date.now() - this._orphanQuestionReplyTime > 500); + } +} + +function printTime(ms: number): string { + let h = 0; + let m = 0; + let s = 0; + if (ms >= 1000) { + s = Math.floor(ms / 1000); + ms -= s * 1000; + } + if (s >= 60) { + m = Math.floor(s / 60); + s -= m * 60; + } + if (m >= 60) { + h = Math.floor(m / 60); + m -= h * 60; + } + const _h = h ? `${h}h` : ``; + const _m = m ? `${m}m` : ``; + const _s = s ? `${s}s` : ``; + const _ms = ms ? `${ms}ms` : ``; + return `${_h}${_m}${_s}${_ms}`; +} diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 4f6727ce502d1..0544c8bfdb809 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -43,6 +43,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _unacknowledgedCharCount: number = 0; public get exitMessage(): string | undefined { return this._exitMessage; } + public get currentTitle(): string { return this._currentTitle; } private readonly _onProcessData = this._register(new Emitter()); public get onProcessData(): Event { return this._onProcessData.event; } @@ -202,9 +203,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _setupTitlePolling(ptyProcess: pty.IPty) { // Send initial timeout async to give event listeners a chance to init - setTimeout(() => { - this._sendProcessTitle(ptyProcess); - }, 0); + setTimeout(() => this._sendProcessTitle(ptyProcess), 0); // Setup polling for non-Windows, for Windows `process` doesn't change if (!platform.isWindows) { this._titleInterval = setInterval(() => { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index bba54eda3213d..414af11b2d6d5 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -12,11 +12,11 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalExternalLinkProvider, ITerminalLink } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; -import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IShellLaunchConfig, ITerminalDimensions } from 'vs/platform/terminal/common/terminal'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a4240de16afe5..deba66dbfc194 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -53,13 +53,13 @@ import { revive } from 'vs/base/common/marshalling'; import { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; -import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; import { InternalTestItem, ITestState, RunTestForProviderRequest, RunTestsRequest, TestIdWithProvider, TestsDiff, ISerializedTestResults } from 'vs/workbench/contrib/testing/common/testCollection'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust'; +import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; export interface IEnvironment { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 393fdb64cfb59..760c725cf6b14 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -9,18 +9,18 @@ import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShap import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ITerminalChildProcess, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/platform/terminal/common/terminal'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { localize } from 'vs/nls'; import { NotSupportedError } from 'vs/base/common/errors'; import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { generateUuid } from 'vs/base/common/uuid'; +import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; +import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index d40c0bd224609..d26f26b1708b0 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -11,11 +11,11 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost import { connectProxyResolver } from 'vs/workbench/services/extensions/node/proxyResolver'; import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadService'; -import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; +import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; class NodeModuleRequireInterceptor extends RequireInterceptor { diff --git a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts index d91501663cc24..025f5289f6a57 100644 --- a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts +++ b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEnvironmentVariableInfo, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff, EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { EnvironmentVariableMutatorType, IEnvironmentVariableInfo, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { localize } from 'vs/nls'; diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 0fe3cfeafd2ff..82d600a6b4a61 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -14,10 +14,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteTerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IRemoteTerminalProcessExecCommandEvent, IShellLaunchConfigDto, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; -import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IRemoteTerminalAttachTarget, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; - +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; export class RemoteTerminalService extends Disposable implements IRemoteTerminalService { public _serviceBrand: undefined; @@ -103,7 +102,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } private _startBarrier: Barrier; - private _remoteTerminalId: number; + private _persistentTerminalId: number; private _inReplay = false; @@ -122,7 +121,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP ) { super(); this._startBarrier = new Barrier(); - this._remoteTerminalId = 0; + this._persistentTerminalId = 0; if (this._isPreconnectionTerminal) { // Add a loading title only if this terminal is @@ -131,7 +130,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP } } - public async start(): Promise { + public async start(): Promise { // Fetch the environment to check shell permissions const env = await this._remoteAgentService.getEnvironment(); if (!env) { @@ -139,7 +138,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP throw new Error('Could not fetch remote environment'); } - if (!this._shellLaunchConfig.remoteAttach) { + if (!this._shellLaunchConfig.attachPersistentTerminal) { const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(env.os); const shellLaunchConfigDto: IShellLaunchConfigDto = { @@ -161,33 +160,33 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP isWorkspaceShellAllowed, ); - this._remoteTerminalId = result.terminalId; + this._persistentTerminalId = result.terminalId; this.setupTerminalEventListener(); this._onProcessResolvedShellLaunchConfig.fire(reviveIShellLaunchConfig(result.resolvedShellLaunchConfig)); - const startResult = await this._remoteTerminalChannel.startTerminalProcess(this._remoteTerminalId); + const startResult = await this._remoteTerminalChannel.startTerminalProcess(this._persistentTerminalId); if (typeof startResult !== 'undefined') { // An error occurred return startResult; } } else { - this._remoteTerminalId = this._shellLaunchConfig.remoteAttach.id; - this._onProcessReady.fire({ pid: this._shellLaunchConfig.remoteAttach.pid, cwd: this._shellLaunchConfig.remoteAttach.cwd }); + this._persistentTerminalId = this._shellLaunchConfig.attachPersistentTerminal.id; + this._onProcessReady.fire({ pid: this._shellLaunchConfig.attachPersistentTerminal.pid, cwd: this._shellLaunchConfig.attachPersistentTerminal.cwd }); this.setupTerminalEventListener(); setTimeout(() => { - this._onProcessTitleChanged.fire(this._shellLaunchConfig.remoteAttach!.title); + this._onProcessTitleChanged.fire(this._shellLaunchConfig.attachPersistentTerminal!.title); }, 0); } this._startBarrier.open(); - return { remoteTerminalId: this._remoteTerminalId }; + return { persistentTerminalId: this._persistentTerminalId }; } public shutdown(immediate: boolean): void { this._startBarrier.wait().then(_ => { - this._remoteTerminalChannel.shutdownTerminalProcess(this._remoteTerminalId, immediate); + this._remoteTerminalChannel.shutdownTerminalProcess(this._persistentTerminalId, immediate); }); } @@ -197,12 +196,12 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP } this._startBarrier.wait().then(_ => { - this._remoteTerminalChannel.sendInputToTerminalProcess(this._remoteTerminalId, data); + this._remoteTerminalChannel.sendInputToTerminalProcess(this._persistentTerminalId, data); }); } private setupTerminalEventListener(): void { - this._register(this._remoteTerminalChannel.onTerminalProcessEvent(this._remoteTerminalId)(event => { + this._register(this._remoteTerminalChannel.onTerminalProcessEvent(this._persistentTerminalId)(event => { switch (event.type) { case 'ready': return this._onProcessReady.fire({ pid: event.pid, cwd: event.cwd }); @@ -235,7 +234,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP case 'execCommand': return this._execCommand(event); case 'orphan?': { - this._remoteTerminalChannel.orphanQuestionReply(this._remoteTerminalId); + this._remoteTerminalChannel.orphanQuestionReply(this._persistentTerminalId); return; } } @@ -248,7 +247,7 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP } this._startBarrier.wait().then(_ => { - this._remoteTerminalChannel.resizeTerminalProcess(this._remoteTerminalId, cols, rows); + this._remoteTerminalChannel.resizeTerminalProcess(this._persistentTerminalId, cols, rows); }); } @@ -259,18 +258,18 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP } this._startBarrier.wait().then(_ => { - this._remoteTerminalChannel.sendCharCountToTerminalProcess(this._remoteTerminalId, charCount); + this._remoteTerminalChannel.sendCharCountToTerminalProcess(this._persistentTerminalId, charCount); }); } public async getInitialCwd(): Promise { await this._startBarrier.wait(); - return this._remoteTerminalChannel.getTerminalInitialCwd(this._remoteTerminalId); + return this._remoteTerminalChannel.getTerminalInitialCwd(this._persistentTerminalId); } public async getCwd(): Promise { await this._startBarrier.wait(); - return this._remoteTerminalChannel.getTerminalCwd(this._remoteTerminalId); + return this._remoteTerminalChannel.getTerminalCwd(this._persistentTerminalId); } /** @@ -285,9 +284,9 @@ export class RemoteTerminalProcess extends Disposable implements ITerminalChildP const commandArgs = event.commandArgs.map(arg => revive(arg)); try { const result = await this._commandService.executeCommand(event.commandId, ...commandArgs); - this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._remoteTerminalId, reqId, false, result); + this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._persistentTerminalId, reqId, false, result); } catch (err) { - this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._remoteTerminalId, reqId, true, err); + this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._persistentTerminalId, reqId, true, err); } } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 91f29c24520f3..0a1da75daf1e6 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -7,14 +7,15 @@ import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; -import { IWindowsShellHelper, ITerminalConfigHelper, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalNativeWindowsDelegate, LinuxDistro, IRemoteTerminalAttachTarget, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ITerminalTabLayoutInfoById } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment, Platform } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { URI } from 'vs/base/common/uri'; -import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; +import { IAvailableShellsRequest, ICommandTracker, IDefaultShellAndArgsRequest, INavigationMode, IRemoteTerminalAttachTarget, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, IWindowsShellHelper, LinuxDistro, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalInstanceService = createDecorator('terminalInstanceService'); @@ -45,9 +46,12 @@ export interface ITerminalInstanceService { getXtermWebglConstructor(): Promise; createWindowsShellHelper(shellProcessId: number, xterm: XTermTerminal): IWindowsShellHelper; createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): Promise; - + attachToProcess(id: number): Promise; getDefaultShellAndArgs(useAutomationShell: boolean, platformOverride?: Platform): Promise<{ shell: string, args: string[] | string | undefined }>; getMainProcessParentEnv(): Promise; + setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void; + setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): void; + getTerminalLayoutInfo(): Promise; } export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper { @@ -273,10 +277,10 @@ export interface ITerminalInstance { processId: number | undefined; /** - * The id of a terminal on the remote server. Defined if this is a terminal created - * by the RemoteTerminalService. + * The id of a persistent terminal. Defined if this is a terminal created + * by the RemoteTerminalService or LocalPtyService. */ - readonly remoteTerminalId: number | undefined; + readonly persistentTerminalId: number | undefined; /** * An event that fires when the terminal instance's title changes. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 319e6b3f6eb94..140aa69c41f5d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -687,7 +687,7 @@ export function registerTerminalActions() { }); const selected = await quickInputService.pick(items, { canPickMany: false }); if (selected) { - const instance = terminalService.createTerminal({ remoteAttach: selected.term }); + const instance = terminalService.createTerminal({ attachPersistentTerminal: selected.term }); terminalService.setActiveInstance(instance); terminalService.showPanel(true); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index cbbab36147ff0..15be9dfd67254 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -42,11 +42,11 @@ import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { EnvironmentVariableInfoWidget } from 'vs/workbench/contrib/terminal/browser/widgets/environmentVariableInfoWidget'; -import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TypeAheadAddon } from 'vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; // How long in milliseconds should an average frame take to render for a notification to appear @@ -373,7 +373,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return TerminalInstance._lastKnownCanvasDimensions; } - public get remoteTerminalId(): number | undefined { return this._processManager.remoteTerminalId; } + public get persistentTerminalId(): number | undefined { return this._processManager.persistentTerminalId; } private async _getXtermConstructor(): Promise { if (xtermConstructor) { @@ -1598,7 +1598,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Recreate the process if the terminal has not yet been interacted with and it's not a // special terminal (eg. task, extension terminal) - if (info.requiresAction && !this._processManager.hasWrittenData && !this._shellLaunchConfig.isFeatureTerminal && !this._shellLaunchConfig.isExtensionTerminal) { + if (info.requiresAction && !this._processManager.hasWrittenData && !this._shellLaunchConfig.isFeatureTerminal && !this._shellLaunchConfig.isExtensionTerminal && !this._shellLaunchConfig.attachPersistentTerminal) { this.relaunch(); return; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index 23020b1146842..9eda7716cc12e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -12,8 +12,9 @@ import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { Emitter } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ITerminalChildProcess } from 'vs/platform/terminal/common/terminal'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ITerminalsLayoutInfoById, ITerminalsLayoutInfo, ITerminalChildProcess } from 'vs/platform/terminal/common/terminal'; +import { IGetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; let Terminal: typeof XTermTerminal; let SearchAddon: typeof XTermSearchAddon; @@ -76,6 +77,22 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst public async getMainProcessParentEnv(): Promise { return {}; } + + getWorkspaceId(): string { + return ''; + } + setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById, id?: string): Promise { + throw new Error('Method not implemented.'); + } + getTerminalLayoutInfo(args?: IGetTerminalLayoutInfoArgs): Promise { + throw new Error('Method not implemented.'); + } + getTerminalLayouts(): Map { + return new Map(); + } + attachToProcess(id: number): Promise { + throw new Error('Method not implemented.'); + } } registerSingleton(ITerminalInstanceService, TerminalInstanceService, true); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index ce47964cff8a2..b48f5ed9eddee 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import * as nls from 'vs/nls'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IShellLaunchConfig, ITerminalLaunchError, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalDimensions } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; let hasReceivedResponseFromRemoteExtHost: boolean = false; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index beeb6039876a5..7fb3cb01cc60d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -23,11 +23,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Disposable } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { IEnvironmentVariableService, IMergedEnvironmentVariableCollection, IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { URI } from 'vs/base/common/uri'; -import { IShellLaunchConfig, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, IProcessDataEvent, ITerminalDimensionsOverride } from 'vs/platform/terminal/common/terminal'; +import { IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants } from 'vs/platform/terminal/common/terminal'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -89,8 +89,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce public get onEnvironmentVariableInfoChanged(): Event { return this._onEnvironmentVariableInfoChange.event; } public get environmentVariableInfo(): IEnvironmentVariableInfo | undefined { return this._environmentVariableInfo; } - private _remoteTerminalId: number | undefined; - public get remoteTerminalId(): number | undefined { return this._remoteTerminalId; } + private _persistentTerminalId: number | undefined; + public get persistentTerminalId(): number | undefined { return this._persistentTerminalId; } public get hasWrittenData(): boolean { return this._hasWrittenData; @@ -188,8 +188,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } else { // Flow control is not needed for ptys hosted in the same process (ie. the electron // renderer). - shellLaunchConfig.flowControl = false; - this._process = await this._launchLocalProcess(shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled); + if (shellLaunchConfig.attachPersistentTerminal) { + this._process = await this._terminalInstanceService.attachToProcess(shellLaunchConfig.attachPersistentTerminal.id); + } else { + this._process = await this._launchLocalProcess(shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled); + } } } @@ -210,8 +213,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._initialCwd = e.cwd; this._onProcessReady.fire(); - // Send any queued data that's waiting if (this._preLaunchInputQueue.length > 0 && this._process) { + // Send any queued data that's waiting this._process.input(this._preLaunchInputQueue.join('')); this._preLaunchInputQueue.length = 0; } @@ -233,8 +236,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce }, LAUNCHING_DURATION); const result = await this._process.start(); - if (result && 'remoteTerminalId' in result) { - this._remoteTerminalId = result.remoteTerminalId; + if (result && 'persistentTerminalId' in result) { + this._persistentTerminalId = result.persistentTerminalId; } else if (result) { // Error return result; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 3c83a0f496204..52b81a739082f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -18,20 +18,20 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IPickOptions, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IShellLaunchConfig, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { TerminalConnectionState, IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalTab, TerminalShellType, WindowsShellType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalConnectionState, IRemoteTerminalService, ITerminalExternalLinkProvider, ITerminalInstance, ITerminalService, ITerminalTab, TerminalShellType, WindowsShellType, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { IAvailableShellsRequest, IRemoteTerminalAttachTarget, IShellDefinition, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, ITerminalsLayoutInfoById, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IShellLaunchConfig, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { IAvailableShellsRequest, IRemoteTerminalAttachTarget, IShellDefinition, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; interface IExtHostReadyEntry { promise: Promise; @@ -67,6 +67,7 @@ export class TerminalService implements ITerminalService { private _terminalContainer: HTMLElement | undefined; private _nativeWindowsDelegate: ITerminalNativeWindowsDelegate | undefined; private _remoteTerminalsInitPromise: Promise | undefined; + private _localTerminalsInitPromise: Promise | undefined; private _connectionState: TerminalConnectionState; public get configHelper(): ITerminalConfigHelper { return this._configHelper; } @@ -119,13 +120,14 @@ export class TerminalService implements ITerminalService { @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService ) { this._activeTabIndex = 0; this._isShuttingDown = false; this._findState = new FindReplaceState(); - lifecycleService.onBeforeShutdown(async event => event.veto(this._onBeforeShutdown(), 'veto.terminal')); - lifecycleService.onShutdown(() => this._onShutdown()); + lifecycleService.onBeforeShutdown(async e => e.veto(this._onBeforeShutdown(), 'veto.terminal')); + lifecycleService.onWillShutdown(e => this._onWillShutdown(e)); this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); this._terminalShellTypeContextKey = KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE.bindTo(this._contextKeyService); this._terminalAltBufferActiveContextKey = KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE.bindTo(this._contextKeyService); @@ -143,22 +145,61 @@ export class TerminalService implements ITerminalService { const enableTerminalReconnection = this.configHelper.config.enablePersistentSessions; const serverSpawn = this.configHelper.config.serverSpawn; + + //TODO@meganrogge: deal with case where there are both local and remote terminals to reconnect if (!!this._environmentService.remoteAuthority && enableTerminalReconnection && serverSpawn) { this._remoteTerminalsInitPromise = this._reconnectToRemoteTerminals(); this._connectionState = TerminalConnectionState.Connecting; + } else if (enableTerminalReconnection) { + this._localTerminalsInitPromise = this._reconnectToLocalTerminals(); + this._connectionState = TerminalConnectionState.Connecting; } else { this._connectionState = TerminalConnectionState.Connected; - this.attachRemoteListeners(); + this.attachProcessLayoutListeners(true); } } private async _reconnectToRemoteTerminals(): Promise { // Reattach to all remote terminals const layoutInfo = await this._remoteTerminalService.getTerminalLayoutInfo(); + const reconnectCounter = this._recreateTerminalTabs(layoutInfo); + /* __GDPR__ + "terminalReconnection" : { + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + const data = { + count: reconnectCounter + }; + this._telemetryService.publicLog('terminalReconnection', data); + this._connectionState = TerminalConnectionState.Connected; + // now that terminals have been restored, + // attach listeners to update remote when terminals are changed + this.attachProcessLayoutListeners(true); + this._onDidChangeConnectionState.fire(); + } + + private async _reconnectToLocalTerminals(): Promise { + // Reattach to all local terminals + const layoutInfo = await this._terminalInstanceService.getTerminalLayoutInfo(); + if (layoutInfo) { + this._recreateTerminalTabs(layoutInfo); + // now that terminals have been restored, + // attach listeners to update local state when terminals are changed + this.attachProcessLayoutListeners(false); + } else { + this.createTerminal(); + this.attachProcessLayoutListeners(false); + } + this._connectionState = TerminalConnectionState.Connected; + this._onDidChangeConnectionState.fire(); + } + + private _recreateTerminalTabs(layoutInfo?: ITerminalsLayoutInfo): number { let reconnectCounter = 0; let activeTab: ITerminalTab | undefined; if (layoutInfo) { - layoutInfo.tabs.forEach((tabLayout) => { + layoutInfo.tabs.forEach(tabLayout => { const terminalLayouts = tabLayout.terminals.filter(t => t.terminal && t.terminal.isOrphan); if (terminalLayouts.length) { reconnectCounter += terminalLayouts.length; @@ -167,17 +208,19 @@ export class TerminalService implements ITerminalService { terminalLayouts.forEach((terminalLayout) => { if (!terminalInstance) { // create tab and terminal - terminalInstance = this.createTerminal({ remoteAttach: terminalLayout.terminal! }); + terminalInstance = this.createTerminal({ attachPersistentTerminal: terminalLayout.terminal! }); tab = this._getTabForInstance(terminalInstance); if (tabLayout.isActive) { activeTab = tab; } } else { // add split terminals to this tab - this.splitInstance(terminalInstance, { remoteAttach: terminalLayout.terminal! }); + this.splitInstance(terminalInstance, { attachPersistentTerminal: terminalLayout.terminal! }); } }); - const activeInstance = this.terminalInstances.find(t => t.shellLaunchConfig.remoteAttach?.pid === tabLayout.activeTerminalProcessId); + const activeInstance = this.terminalInstances.find(t => { + return t.shellLaunchConfig.attachPersistentTerminal?.id === tabLayout.activePersistentTerminalId; + }); if (activeInstance) { this.setActiveInstance(activeInstance); } @@ -188,32 +231,13 @@ export class TerminalService implements ITerminalService { this.setActiveTabByIndex(activeTab ? this.terminalTabs.indexOf(activeTab) : 0); } } - /* __GDPR__ - "terminalReconnection" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - const data = { - count: reconnectCounter - }; - this._telemetryService.publicLog('terminalReconnection', data); - this._connectionState = TerminalConnectionState.Connected; - // now that terminals have been restored, - // attach listeners to update remote when terminals are changed - this.attachRemoteListeners(); - this._onDidChangeConnectionState.fire(); + return reconnectCounter; } - private attachRemoteListeners(): void { - this.onActiveTabChanged(() => { - this._updateRemoteState(); - }); - this.onActiveInstanceChanged(() => { - this._updateRemoteState(); - }); - this.onInstancesChanged(() => { - this._updateRemoteState(); - }); + private attachProcessLayoutListeners(isRemote: boolean): void { + this.onActiveTabChanged(() => isRemote ? this._updateRemoteState() : this._updateLocalState()); + this.onActiveInstanceChanged(() => isRemote ? this._updateRemoteState() : this._updateLocalState()); + this.onInstancesChanged(() => isRemote ? this._updateRemoteState() : this._updateLocalState()); } public setNativeWindowsDelegate(delegate: ITerminalNativeWindowsDelegate): void { @@ -300,8 +324,13 @@ export class TerminalService implements ITerminalService { return veto; } - private _onShutdown(): void { - // Dispose of all instances + private _onWillShutdown(e: WillShutdownEvent): void { + // Don't touch processes if the shutdown was a result of reload as they will be reattached + if (e.reason === ShutdownReason.RELOAD) { + return; + } + + // Force dispose of all terminal instances this.terminalInstances.forEach(instance => instance.dispose(true)); } @@ -323,6 +352,14 @@ export class TerminalService implements ITerminalService { } } + @debounce(500) + private _updateLocalState(): void { + const state: ITerminalsLayoutInfoById = { + tabs: this.terminalTabs.map(t => t.getLayoutInfo(t === this.getActiveTab())) + }; + this._terminalInstanceService.setTerminalLayoutInfo(state); + } + private _removeTab(tab: ITerminalTab): void { // Get the index of the tab and remove it from the list const index = this._terminalTabs.indexOf(tab); @@ -443,10 +480,14 @@ export class TerminalService implements ITerminalService { await this._remoteTerminalsInitPromise; if (!this.terminalTabs.length) { - this.createTerminal(undefined); + this.createTerminal(); } - } else if (this.terminalTabs.length === 0) { - // Local window, or remote terminal reconnection is disabled, just create a terminal + } else if (this._localTerminalsInitPromise) { + await this._localTerminalsInitPromise; + if (!this.terminalTabs.length) { + this.createTerminal(); + } + } else if (!this.terminalTabs.length) { this.createTerminal(); } } @@ -725,7 +766,6 @@ export class TerminalService implements ITerminalService { return new Promise(r => this._onRequestAvailableShells.fire({ callback: r })); } - public createInstance(container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance { const instance = this._instantiationService.createInstance(TerminalInstance, this._terminalFocusContextKey, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts index d9135de0a43c8..acecc12f493b9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITerminalTabLayoutInfoById, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { SplitView, Orientation, IView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; @@ -11,7 +11,7 @@ import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITerminalInstance, Direction, ITerminalTab, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; -import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal'; const SPLIT_PANE_MIN_SIZE = 120; @@ -299,15 +299,15 @@ export class TerminalTab extends Disposable implements ITerminalTab { public getLayoutInfo(isActive: boolean): ITerminalTabLayoutInfoById { const isHorizontal = this.splitPaneContainer?.orientation === Orientation.HORIZONTAL; - const remoteInstances = this.terminalInstances.filter(instance => typeof instance.remoteTerminalId === 'number'); - const totalSize = remoteInstances.map(instance => isHorizontal ? instance.cols : instance.rows).reduce((totalValue, currentValue) => totalValue + currentValue, 0); + const instances = this.terminalInstances.filter(instance => typeof instance.persistentTerminalId === 'number'); + const totalSize = instances.map(instance => isHorizontal ? instance.cols : instance.rows).reduce((totalValue, currentValue) => totalValue + currentValue, 0); return { isActive: isActive, - activeTerminalProcessId: this.activeInstance?.processId || 0, - terminals: remoteInstances.map(t => { + activePersistentTerminalId: this.activeInstance ? this.activeInstance.persistentTerminalId : undefined, + terminals: instances.map(t => { return { relativeSize: isHorizontal ? t.cols / totalSize : t.rows / totalSize, - terminal: t.remoteTerminalId! + terminal: t.persistentTerminalId ? t.persistentTerminalId : t.id }; }) }; @@ -481,10 +481,13 @@ export class TerminalTab extends Disposable implements ITerminalTab { } public resizePanes(relativeSizes: number[]): void { + //TODO@meganrogge make sure this doesn't break remote case if (!this._splitPaneContainer) { this._initialRelativeSizes = relativeSizes; return; } + // for the local case + this._initialRelativeSizes = relativeSizes; this._splitPaneContainer.resizePanes(relativeSizes); } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.contribution.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.contribution.ts index 4ebe68228164a..7fd4e9517548f 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.contribution.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.contribution.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariableService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariable'; registerSingleton(IEnvironmentVariableService, EnvironmentVariableService, true); diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts index 9db5c7b7f8786..a64ef609d290d 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEnvironmentVariableCollection, EnvironmentVariableMutatorType, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff, IExtensionOwnedEnvironmentVariableMutator } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; +import { EnvironmentVariableMutatorType, IEnvironmentVariableCollection, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/workbench/contrib/terminal/common/environmentVariable'; export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection { readonly map: Map = new Map(); diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts index e1d98440757b5..402f812511e71 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEnvironmentVariableService, IMergedEnvironmentVariableCollection, ISerializableEnvironmentVariableCollection, IEnvironmentVariableCollectionWithPersistence } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { Event, Emitter } from 'vs/base/common/event'; import { debounce, throttle } from 'vs/base/common/decorators'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { IEnvironmentVariableCollectionWithPersistence, IEnvironmentVariableService, IMergedEnvironmentVariableCollection, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; const ENVIRONMENT_VARIABLE_COLLECTIONS_KEY = 'terminal.integrated.environmentVariableCollections'; diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index 9358cbf6d6ffd..c9c3022b2a4ec 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -11,15 +11,16 @@ import { IWorkbenchConfigurationService } from 'vs/workbench/services/configurat import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -import { IRawTerminalTabLayoutInfo, ITerminalConfiguration, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ITerminalTabLayoutInfoById, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Schemas } from 'vs/base/common/network'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ITerminalEnvironment, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; +import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; export const REMOTE_TERMINAL_CHANNEL_NAME = 'remoteterminal'; @@ -130,15 +131,6 @@ export interface IListTerminalsArgs { isInitialization: boolean; } -export interface ISetTerminalLayoutInfoArgs { - workspaceId: string; - tabs: ITerminalTabLayoutInfoById[]; -} - -export interface IGetTerminalLayoutInfoArgs { - workspaceId: string; -} - export interface IRemoteTerminalDescriptionDto { id: number; pid: number; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 7ad8acd224694..976054b0c33be 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -9,9 +9,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; -import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; +import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; export const TERMINAL_VIEW_ID = 'terminal'; @@ -179,30 +179,6 @@ export interface IRemoteTerminalAttachTarget { isOrphan: boolean; } -export interface IRawTerminalInstanceLayoutInfo { - relativeSize: number; - terminal: T; -} - -export type ITerminalInstanceLayoutInfoById = IRawTerminalInstanceLayoutInfo; -export type ITerminalInstanceLayoutInfo = IRawTerminalInstanceLayoutInfo; - -export interface IRawTerminalTabLayoutInfo { - isActive: boolean; - activeTerminalProcessId: number; - terminals: IRawTerminalInstanceLayoutInfo[]; -} - -export type ITerminalTabLayoutInfoById = IRawTerminalTabLayoutInfo; -export type ITerminalTabLayoutInfo = IRawTerminalTabLayoutInfo; - -export interface IRawTerminalsLayoutInfo { - tabs: IRawTerminalTabLayoutInfo[]; -} - -export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo; -export type ITerminalsLayoutInfoById = IRawTerminalsLayoutInfo; - /** * Provides access to native Windows calls that can be injected into non-native layers. */ @@ -218,11 +194,6 @@ export interface ITerminalNativeWindowsDelegate { getWslPath(path: string): Promise; } -export interface IShellDefinition { - label: string; - path: string; -} - export interface ICommandTracker { scrollToPreviousCommand(): void; scrollToNextCommand(): void; @@ -246,6 +217,27 @@ export interface IBeforeProcessDataEvent { data: string; } +export interface IShellDefinition { + label: string; + path: string; +} + +export interface IAvailableShellsRequest { + callback: (shells: IShellDefinition[]) => void; +} + + +export interface IDefaultShellAndArgsRequest { + useAutomationShell: boolean; + callback: (shell: string, args: string[] | string | undefined) => void; +} + +export interface IWindowsShellHelper extends IDisposable { + readonly onShellNameChange: Event; + + getShellName(): Promise; +} + export interface ITerminalProcessManager extends IDisposable { readonly processState: ProcessState; readonly ptyProcessReady: Promise; @@ -254,7 +246,7 @@ export interface ITerminalProcessManager extends IDisposable { readonly os: OperatingSystem | undefined; readonly userHome: string | undefined; readonly environmentVariableInfo: IEnvironmentVariableInfo | undefined; - readonly remoteTerminalId: number | undefined; + readonly persistentTerminalId: number | undefined; /** Whether the process has had data written to it yet. */ readonly hasWrittenData: boolean; diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 17b81211dfda7..48b4f7fe15e6d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -7,10 +7,10 @@ import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { URI as Uri } from 'vs/base/common/uri'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; import { ILogService } from 'vs/platform/log/common/log'; +import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; /** * This module contains utility functions related to the environment, cwd and paths. diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index ac7b80ade72dd..f4fb3ec06ae8b 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -22,10 +22,12 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ILogService } from 'vs/platform/log/common/log'; import { getSystemShell } from 'vs/base/node/shell'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; -import { IShellLaunchConfig, ITerminalChildProcess } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; import { LocalPty } from 'vs/workbench/contrib/terminal/electron-sandbox/localPty'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { localize } from 'vs/nls'; @@ -51,6 +53,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst @IHistoryService private readonly _historyService: IHistoryService, @ILogService private readonly _logService: ILogService, @ILocalPtyService private readonly _localPtyService: ILocalPtyService, + @ILabelService private readonly _labelService: ILabelService, @INotificationService notificationService: INotificationService ) { super(); @@ -110,7 +113,12 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst } public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): Promise { - const id = await this._localPtyService.createProcess(shellLaunchConfig, cwd, cols, rows, env, process.env as IProcessEnvironment, windowsEnableConpty); + const id = await this._localPtyService.createProcess(shellLaunchConfig, cwd, cols, rows, env, process.env as IProcessEnvironment, windowsEnableConpty, this._getWorkspaceId(), this._getWorkspaceName()); + return this._instantiationService.createInstance(LocalPty, id); + } + + public async attachToProcess(id: number): Promise { + await this._localPtyService.attachToProcess(id); return this._instantiationService.createInstance(LocalPty, id); } @@ -148,4 +156,28 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst public getMainProcessParentEnv(): Promise { return getMainProcessParentEnv(); } + + public setTerminalLayoutInfo(layoutInfo: ITerminalsLayoutInfoById): void { + const args: ISetTerminalLayoutInfoArgs = { + workspaceId: this._getWorkspaceId(), + tabs: layoutInfo.tabs + }; + this._localPtyService.setTerminalLayoutInfo(args); + } + + public async getTerminalLayoutInfo(): Promise { + const layoutArgs: IGetTerminalLayoutInfoArgs = { + workspaceId: this._getWorkspaceId() + }; + let result = await this._localPtyService.getTerminalLayoutInfo(layoutArgs); + return result; + } + + private _getWorkspaceId(): string { + return this._workspaceContextService.getWorkspace().id; + } + + private _getWorkspaceName(): string { + return this._labelService.getWorkspaceLabel(this._workspaceContextService.getWorkspace()); + } } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts index 09bbb9a43fc7e..1fc830d01c8fd 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts @@ -5,16 +5,21 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IShellLaunchConfig, ITerminalLaunchError, ITerminalChildProcess, ITerminalDimensionsOverride, IProcessDataEvent } from 'vs/platform/terminal/common/terminal'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; +import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; /** * Responsible for establishing and maintaining a connection with an existing terminal process * created on the local pty host. */ export class LocalPty extends Disposable implements ITerminalChildProcess { + private _inReplay = false; + private readonly _onProcessData = this._register(new Emitter()); public readonly onProcessData = this._onProcessData.event; + private readonly _onProcessReplay = this._register(new Emitter()); + public readonly onProcessReplay = this._onProcessReplay.event; private readonly _onProcessExit = this._register(new Emitter()); public readonly onProcessExit = this._onProcessExit.event; private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); @@ -37,6 +42,30 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { this._localPtyService.onProcessTitleChanged(e => e.id === this._localPtyId && this._onProcessTitleChanged.fire(e.event)); this._localPtyService.onProcessOverrideDimensions(e => e.id === this._localPtyId && this._onProcessOverrideDimensions.fire(e.event)); this._localPtyService.onProcessResolvedShellLaunchConfig(e => e.id === this._localPtyId && this._onProcessResolvedShellLaunchConfig.fire(e.event)); + this._localPtyService.onProcessReplay(e => { + if (e.id !== this._localPtyId) { + return; + } + try { + this._inReplay = true; + + for (const innerEvent of e.event.events) { + if (innerEvent.cols !== 0 || innerEvent.rows !== 0) { + // never override with 0x0 as that is a marker for an unknown initial size + this._onProcessOverrideDimensions.fire({ cols: innerEvent.cols, rows: innerEvent.rows, forceExactSize: true }); + } + this._onProcessData.fire({ data: innerEvent.data, sync: true }); + } + } finally { + this._inReplay = false; + } + + // remove size override + this._onProcessOverrideDimensions.fire(undefined); + + return; + }); + if (this._localPtyService.onPtyHostExit) { this._localPtyService.onPtyHostExit(() => { this._onProcessExit.fire(undefined); @@ -44,35 +73,38 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { } } - start(): Promise { + start(): Promise { return this._localPtyService.start(this._localPtyId); } - shutdown(immediate: boolean): void { this._localPtyService.shutdown(this._localPtyId, immediate); } - input(data: string): void { + if (this._inReplay) { + return; + } this._localPtyService.input(this._localPtyId, data); } - resize(cols: number, rows: number): void { + if (this._inReplay) { + return; + } this._localPtyService.resize(this._localPtyId, cols, rows); } - - acknowledgeDataEvent(charCount: number): void { - this._localPtyService.acknowledgeDataEvent(this._localPtyId, charCount); - } - getInitialCwd(): Promise { return this._localPtyService.getInitialCwd(this._localPtyId); } - getCwd(): Promise { return this._localPtyService.getCwd(this._localPtyId); } - getLatency(): Promise { + // TODO: The idea here was to add the result plus the time it took to get the latency return this._localPtyService.getLatency(this._localPtyId); } + acknowledgeDataEvent(charCount: number): void { + if (this._inReplay) { + return; + } + this._localPtyService.acknowledgeDataEvent(this._localPtyId, charCount); + } } diff --git a/src/vs/workbench/contrib/terminal/node/terminal.ts b/src/vs/workbench/contrib/terminal/node/terminal.ts index 11e429a69d678..2a135cb5230ce 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -6,11 +6,11 @@ import * as fs from 'fs'; import * as platform from 'vs/base/common/platform'; import { SymlinkSupport } from 'vs/base/node/pfs'; -import { LinuxDistro, IShellDefinition } from 'vs/workbench/contrib/terminal/common/terminal'; import { coalesce } from 'vs/base/common/arrays'; import { normalize, basename } from 'vs/base/common/path'; import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; import { getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; +import { IShellDefinition, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; let detectedDistro = LinuxDistro.Unknown; if (platform.isLinux) { diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts index 6a2e3b93b2125..fe56ae5a7d83d 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Emitter } from 'vs/base/common/event'; -import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; +import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));