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 df71e4f6b62..99d7b4211f1 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -9,8 +9,7 @@ import { assertNoRpc } from '../utils'; // Disable terminal tests: // - Web https://github.com/microsoft/vscode/issues/92826 -// - Remote https://github.com/microsoft/vscode/issues/96057 -((env.uiKind === UIKind.Web || typeof env.remoteName !== 'undefined') ? suite.skip : suite)('vscode API - terminal', () => { +(env.uiKind === UIKind.Web ? suite.skip : suite)('vscode API - terminal', () => { let extensionContext: ExtensionContext; suiteSetup(async () => { @@ -25,6 +24,8 @@ import { assertNoRpc } from '../utils'; await config.update('showExitAlert', false, ConfigurationTarget.Global); // Canvas may cause problems when running in a container await config.update('rendererType', 'dom', ConfigurationTarget.Global); + // Disable env var relaunch for tests to prevent terminals relaunching themselves + await config.update('environmentChangesRelaunch', false, ConfigurationTarget.Global); }); suite('Terminal', () => { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index a2684538707..5ec438a1714 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -4,13 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, env, UIKind, ShellExecution, TaskExecution, Terminal, Event } from 'vscode'; +import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, env, UIKind, ShellExecution, TaskExecution, Terminal, Event, workspace, ConfigurationTarget } from 'vscode'; import { assertNoRpc } from '../utils'; // Disable tasks tests: // - Web https://github.com/microsoft/vscode/issues/90528 ((env.uiKind === UIKind.Web) ? suite.skip : suite)('vscode API - tasks', () => { + suiteSetup(async () => { + 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 + await config.update('rendererType', 'dom', ConfigurationTarget.Global); + // Disable env var relaunch for tests to prevent terminals relaunching themselves + await config.update('environmentChangesRelaunch', false, ConfigurationTarget.Global); + }); + suite('Tasks', () => { let disposables: Disposable[] = []; @@ -206,7 +218,6 @@ import { assertNoRpc } from '../utils'; progressMade.fire(); } })); - taskExecution = await tasks.executeTask(task); executeDoneEvent.fire(); }); diff --git a/remote/package.json b/remote/package.json index 22ac93ce46d..72a71f5ab15 100644 --- a/remote/package.json +++ b/remote/package.json @@ -31,6 +31,7 @@ }, "optionalDependencies": { "vscode-windows-ca-certs": "0.3.0", - "vscode-windows-registry": "1.0.2" + "vscode-windows-registry": "1.0.2", + "windows-process-tree": "0.2.4" } } diff --git a/remote/yarn.lock b/remote/yarn.lock index 8bb9d25675a..cdf9201b7ce 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -253,6 +253,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +nan@^2.13.2: + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -411,6 +416,13 @@ vscode-windows-registry@1.0.2: resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.2.tgz#b863e704a6a69c50b3098a55fbddbe595b0c124a" integrity sha512-/CLLvuOSM2Vme2z6aNyB+4Omd7hDxpf4Thrt8ImxnXeQtxzel2bClJpFQvQqK/s4oaXlkBKS7LqVLeZM+uSVIA== +windows-process-tree@0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/windows-process-tree/-/windows-process-tree-0.2.4.tgz#747af587b54cc6c996f2be0836cc8a8fd0dc038f" + integrity sha512-9gag9AHm3Iin/4YC1EwoIfZlqW/rG2eV7rJZ4Fy5NnAMGdewmnwsie5Rz+CJo2vSolqzzfw7hPeu3oOdniNejg== + dependencies: + nan "^2.13.2" + xterm-addon-search@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0.tgz#e33eab918df7eac7e7baf95dd2b3d14133754881" diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index ee91dcbda94..c75cee2ef0a 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -81,7 +81,7 @@ import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/err import { toErrorMessage } from 'vs/base/common/errorMessage'; import { join } from 'vs/base/common/path'; import { TerminalIpcChannels } from 'vs/platform/terminal/common/terminal'; -import { LocalPtyService } from 'vs/platform/terminal/electron-browser/localPtyService'; +import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; import { IChecksumService } from 'vs/platform/checksum/common/checksumService'; @@ -267,8 +267,7 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); // Terminal - const localPtyService = this._register(new LocalPtyService(logService)); - services.set(ILocalPtyService, localPtyService); + services.set(ILocalPtyService, this._register(new PtyHostService(logService))); return new InstantiationService(services); } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index f018b585996..12510a3c6e6 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -7,7 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' 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'; +import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; export enum WindowsShellType { CommandPrompt = 'cmd', @@ -76,8 +76,6 @@ export enum TerminalIpcChannels { export interface IOffProcessTerminalService { readonly _serviceBrand: undefined; - /** Fired when the ptyHost process goes down, losing all connections to the service's ptys. */ - onPtyHostExit: Event; /** * Fired when the ptyHost process becomes non-responsive, this should disable stdin for all * terminals using this pty host connection and mark them as disconnected. @@ -94,16 +92,18 @@ export interface IOffProcessTerminalService { */ onPtyHostRestart: Event; - createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise; attachToProcess(id: number): Promise; - setTerminalLayoutInfo(args?: ISetTerminalLayoutInfoArgs): void; - setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): void; + listProcesses(reduceGraceTime?: boolean): Promise; + setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise; getTerminalLayoutInfo(): Promise; } export const ILocalTerminalService = createDecorator('localTerminalService'); -export interface ILocalTerminalService extends IOffProcessTerminalService { } +export interface ILocalTerminalService extends IOffProcessTerminalService { + createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise; +} +export const IPtyService = createDecorator('ptyService'); export interface IPtyService { readonly _serviceBrand: undefined; @@ -120,6 +120,7 @@ export interface IPtyService { readonly onProcessOverrideDimensions: Event<{ id: number, event: ITerminalDimensionsOverride | undefined }>; readonly onProcessResolvedShellLaunchConfig: Event<{ id: number, event: IShellLaunchConfig }>; readonly onProcessReplay: Event<{ id: number, event: IPtyHostProcessReplayEvent }>; + readonly onProcessOrphanQuestion: Event<{ id: number }>; restartPtyHost?(): Promise; shutdownAll?(): Promise; @@ -139,6 +140,13 @@ export interface IPtyService { attachToProcess(id: number): Promise; detachFromProcess(id: number): Promise; + /** + * Lists all orphaned processes, ie. those without a connected frontend. + * @param reduceGraceTime Whether to reduce the reconnection grace time for all orphaned + * terminals. + */ + listProcesses(reduceGraceTime: boolean): Promise; + start(id: number): Promise; shutdown(id: number, immediate: boolean): Promise; input(id: number, data: string): Promise; @@ -147,8 +155,10 @@ export interface IPtyService { getCwd(id: number): Promise; getLatency(id: number): Promise; acknowledgeDataEvent(id: number, charCount: number): Promise; + /** Confirm the process is _not_ an orphan. */ + orphanQuestionReply(id: number): Promise; - setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void; + setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise; getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise; } @@ -346,7 +356,7 @@ export const enum LocalReconnectConstants { /** * If there is no reconnection within this time-frame, consider the connection permanently closed... */ - ReconnectionGraceTime = 30000, // 30 seconds + ReconnectionGraceTime = 60000, // 60 seconds /** * Maximal grace time between the first and the last reconnection... */ diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index 2eaec9b1b51..3358e9189b2 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -57,7 +57,7 @@ export interface IGetTerminalLayoutInfoArgs { workspaceId: string; } -export interface IPtyHostDescriptionDto { +export interface IProcessDetails { id: number; pid: number; title: string; @@ -67,7 +67,7 @@ export interface IPtyHostDescriptionDto { isOrphan: boolean; } -export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; +export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; export interface ReplayEntry { cols: number; rows: number; data: string; } export interface IPtyHostProcessReplayEvent { diff --git a/src/vs/platform/terminal/electron-browser/localPtyService.ts b/src/vs/platform/terminal/node/ptyHostService.ts similarity index 91% rename from src/vs/platform/terminal/electron-browser/localPtyService.ts rename to src/vs/platform/terminal/node/ptyHostService.ts index 7d5e9bf9a3f..1b85a1386cc 100644 --- a/src/vs/platform/terminal/electron-browser/localPtyService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -12,7 +12,7 @@ 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'; +import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; enum Constants { MaxRestarts = 5 @@ -24,7 +24,11 @@ enum Constants { */ let lastPtyId = 0; -export class LocalPtyService extends Disposable implements IPtyService { +/** + * This service implements IPtyService by launching a pty host process, forwarding messages to and + * from the pty host process and manages the connection. + */ +export class PtyHostService extends Disposable implements IPtyService { declare readonly _serviceBrand: undefined; private _client: Client; @@ -62,6 +66,8 @@ export class LocalPtyService 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; + private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>()); + readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; constructor( @ILogService private readonly _logService: ILogService @@ -122,6 +128,7 @@ export class LocalPtyService extends Disposable implements IPtyService { 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))); + this._register(proxy.onProcessOrphanQuestion(e => this._onProcessOrphanQuestion.fire(e))); return [client, proxy]; } @@ -144,6 +151,9 @@ export class LocalPtyService extends Disposable implements IPtyService { detachFromProcess(id: number): Promise { return this._proxy.detachFromProcess(id); } + listProcesses(reduceGraceTime: boolean): Promise { + return this._proxy.listProcesses(reduceGraceTime); + } start(id: number): Promise { return this._proxy.start(id); @@ -169,7 +179,11 @@ export class LocalPtyService extends Disposable implements IPtyService { getLatency(id: number): Promise { return this._proxy.getLatency(id); } - setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void { + orphanQuestionReply(id: number): Promise { + return this._proxy.orphanQuestionReply(id); + } + + setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { return this._proxy.setTerminalLayoutInfo(args); } async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise { diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index e9259bf9971..3dfcaf8a5a5 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -10,7 +10,7 @@ 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 { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IProcessDetails, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; import { ILogService } from 'vs/platform/log/common/log'; type WorkspaceId = string; @@ -40,6 +40,8 @@ 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; + private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>()); + readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; constructor( private _lastPtyId: number, @@ -93,6 +95,7 @@ export class PtyService extends Disposable implements IPtyService { persistentProcess.onProcessReady(event => this._onProcessReady.fire({ id, event })); persistentProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event })); persistentProcess.onProcessShellTypeChanged(event => this._onProcessShellTypeChanged.fire({ id, event })); + persistentProcess.onProcessOrphanQuestion(() => this._onProcessOrphanQuestion.fire({ id })); this._ptys.set(id, persistentProcess); return id; } @@ -110,6 +113,21 @@ export class PtyService extends Disposable implements IPtyService { this._throwIfNoPty(id).detach(); } + async listProcesses(reduceGraceTime: boolean): Promise { + if (reduceGraceTime) { + for (const pty of this._ptys.values()) { + pty.reduceGraceTime(); + } + } + + const persistentProcesses = Array.from(this._ptys.entries()).filter(([_, pty]) => pty.shouldPersistTerminal); + + this._logService.info(`Listing ${persistentProcesses.length} persistent terminals, ${this._ptys.size} total terminals`); + const promises = persistentProcesses.map(async ([id, terminalProcessData]) => this._buildProcessDetails(id, terminalProcessData)); + const allTerminals = await Promise.all(promises); + return allTerminals.filter(entry => entry.isOrphan); + } + async start(id: number): Promise { return this._throwIfNoPty(id).start(); } @@ -134,6 +152,9 @@ export class PtyService extends Disposable implements IPtyService { async getLatency(id: number): Promise { return 0; } + async orphanQuestionReply(id: number): Promise { + return this._throwIfNoPty(id).orphanQuestionReply(); + } async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { this._workspaceLayoutInfos.set(args.workspaceId, args); @@ -153,7 +174,7 @@ export class PtyService extends Disposable implements IPtyService { 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[]; + const filtered = expandedTerminals.filter(term => term.terminal !== null) as IRawTerminalInstanceLayoutInfo[]; return { isActive: tab.isActive, activePersistentProcessId: tab.activePersistentProcessId, @@ -161,12 +182,12 @@ export class PtyService extends Disposable implements IPtyService { }; } - private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise> { + private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise> { try { const persistentProcess = this._throwIfNoPty(t.terminal); - const termDto = persistentProcess && await this._terminalToDto(t.terminal, persistentProcess); + const processDetails = persistentProcess && await this._buildProcessDetails(t.terminal, persistentProcess); return { - terminal: termDto ?? null, + terminal: processDetails ?? null, relativeSize: t.relativeSize }; } catch (e) { @@ -179,7 +200,7 @@ export class PtyService extends Disposable implements IPtyService { } } - private async _terminalToDto(id: number, persistentProcess: PersistentTerminalProcess): Promise { + private async _buildProcessDetails(id: number, persistentProcess: PersistentTerminalProcess): Promise { const [cwd, isOrphan] = await Promise.all([persistentProcess.getCwd(), persistentProcess.isOrphaned()]); return { id, @@ -228,6 +249,8 @@ export class PersistentTerminalProcess extends Disposable { readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event; private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; + private readonly _onProcessOrphanQuestion = this._register(new Emitter()); + readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event; private _inReplay = false; @@ -363,7 +386,7 @@ export class PersistentTerminalProcess extends Disposable { this._pendingCommands.delete(reqId); } - async orphanQuestionReply(): Promise { + orphanQuestionReply(): void { this._orphanQuestionReplyTime = Date.now(); if (this._orphanQuestionBarrier) { const barrier = this._orphanQuestionBarrier; @@ -388,19 +411,17 @@ export class PersistentTerminalProcess extends Disposable { } private async _isOrphaned(): Promise { + // The process is already known to be orphaned if (this._disconnectRunner1.isScheduled() || this._disconnectRunner2.isScheduled()) { return true; } + // Ask whether the renderer(s) whether the process is orphaned and await the reply 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); + this._onProcessOrphanQuestion.fire(); } await this._orphanQuestionBarrier.wait(); diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts new file mode 100644 index 00000000000..a3a04aa97aa --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Barrier } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; +import { RemoteTerminalChannelClient } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; + +export class RemotePty extends Disposable implements ITerminalChildProcess { + + public readonly _onProcessData = this._register(new Emitter()); + public readonly onProcessData: Event = this._onProcessData.event; + private readonly _onProcessExit = this._register(new Emitter()); + public readonly onProcessExit: Event = this._onProcessExit.event; + public readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); + public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } + private readonly _onProcessTitleChanged = this._register(new Emitter()); + public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; + private readonly _onProcessShellTypeChanged = this._register(new Emitter()); + public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; + private readonly _onProcessOverrideDimensions = this._register(new Emitter()); + public readonly onProcessOverrideDimensions: Event = this._onProcessOverrideDimensions.event; + private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); + public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } + + private _startBarrier: Barrier; + + private _inReplay = false; + + public get id(): number { return this._id; } + + constructor( + private _id: number, + readonly shouldPersist: boolean, + private readonly _remoteTerminalChannel: RemoteTerminalChannelClient, + private readonly _remoteAgentService: IRemoteAgentService, + private readonly _logService: ILogService + ) { + super(); + this._startBarrier = new Barrier(); + } + + public async start(): Promise { + // Fetch the environment to check shell permissions + const env = await this._remoteAgentService.getEnvironment(); + if (!env) { + // Extension host processes are only allowed in remote extension hosts currently + throw new Error('Could not fetch remote environment'); + } + + this._logService.trace('Spawning remote agent process', { terminalId: this._id }); + + const startResult = await this._remoteTerminalChannel.start(this._id); + + if (typeof startResult !== 'undefined') { + // An error occurred + return startResult; + } + + this._startBarrier.open(); + return undefined; + } + + public shutdown(immediate: boolean): void { + this._startBarrier.wait().then(_ => { + this._remoteTerminalChannel.shutdown(this._id, immediate); + }); + } + + public input(data: string): void { + if (this._inReplay) { + return; + } + + this._startBarrier.wait().then(_ => { + this._remoteTerminalChannel.input(this._id, data); + }); + } + + public resize(cols: number, rows: number): void { + if (this._inReplay) { + return; + } + this._startBarrier.wait().then(_ => { + + this._remoteTerminalChannel.resize(this._id, cols, rows); + }); + } + + public acknowledgeDataEvent(charCount: number): void { + // Support flow control for server spawned processes + if (this._inReplay) { + return; + } + + this._startBarrier.wait().then(_ => { + this._remoteTerminalChannel.acknowledgeDataEvent(this._id, charCount); + }); + } + + public async getInitialCwd(): Promise { + await this._startBarrier.wait(); + return this._remoteTerminalChannel.getInitialCwd(this._id); + } + + public async getCwd(): Promise { + await this._startBarrier.wait(); + return this._remoteTerminalChannel.getCwd(this._id); + } + + handleData(e: string | IProcessDataEvent) { + this._onProcessData.fire(e); + } + handleExit(e: number | undefined) { + this._onProcessExit.fire(e); + } + handleReady(e: { pid: number, cwd: string }) { + this._onProcessReady.fire(e); + } + handleTitleChanged(e: string) { + this._onProcessTitleChanged.fire(e); + } + handleShellTypeChanged(e: TerminalShellType | undefined) { + this._onProcessShellTypeChanged.fire(e); + } + handleOverrideDimensions(e: ITerminalDimensionsOverride | undefined) { + this._onProcessOverrideDimensions.fire(e); + } + handleResolvedShellLaunchConfig(e: IShellLaunchConfig) { + // Revive the cwd URI + if (e.cwd && typeof e.cwd !== 'string') { + e.cwd = URI.revive(e.cwd); + } + this._onProcessResolvedShellLaunchConfig.fire(e); + } + + handleReplay(e: IPtyHostProcessReplayEvent) { + try { + this._inReplay = true; + for (const innerEvent of e.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); + } + + handleOrphanQuestion() { + this._remoteTerminalChannel.orphanQuestionReply(this._id); + } + + public async getLatency(): Promise { + return 0; + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index c9ca7e28d74..38efe01d53d 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -3,26 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Barrier } from 'vs/base/common/async'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; +import { IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { RemotePty } from 'vs/workbench/contrib/terminal/browser/remotePty'; 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 { IShellLaunchConfigDto, RemoteTerminalChannelClient, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; 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, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; export class RemoteTerminalService extends Disposable implements IRemoteTerminalService { public _serviceBrand: undefined; + private readonly _ptys: Map = new Map(); private readonly _remoteTerminalChannel: RemoteTerminalChannelClient | null; - private _hasConnectedToRemote = false; + private _isPtyHostUnresponsive: boolean = false; + + private readonly _onPtyHostUnresponsive = this._register(new Emitter()); + readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event; + private readonly _onPtyHostResponsive = this._register(new Emitter()); + readonly onPtyHostResponsive = this._onPtyHostResponsive.event; + private readonly _onPtyHostRestart = this._register(new Emitter()); + readonly onPtyHostRestart = this._onPtyHostRestart.event; constructor( @ITerminalInstanceService readonly terminalInstanceService: ITerminalInstanceService, @@ -30,34 +39,137 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal @ILogService private readonly _logService: ILogService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICommandService private readonly _commandService: ICommandService, + @INotificationService notificationService: INotificationService ) { super(); + const connection = this._remoteAgentService.getConnection(); if (connection) { - this._remoteTerminalChannel = this._instantiationService.createInstance(RemoteTerminalChannelClient, connection.remoteAuthority, connection.getChannel(REMOTE_TERMINAL_CHANNEL_NAME)); + const channel = this._instantiationService.createInstance(RemoteTerminalChannelClient, connection.remoteAuthority, connection.getChannel(REMOTE_TERMINAL_CHANNEL_NAME)); + this._remoteTerminalChannel = channel; + + channel.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event)); + channel.onProcessExit(e => { + const pty = this._ptys.get(e.id); + if (pty) { + pty.handleExit(e.event); + this._ptys.delete(e.id); + } + }); + channel.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); + channel.onProcessTitleChanged(e => this._ptys.get(e.id)?.handleTitleChanged(e.event)); + channel.onProcessShellTypeChanged(e => this._ptys.get(e.id)?.handleShellTypeChanged(e.event)); + channel.onProcessOverrideDimensions(e => this._ptys.get(e.id)?.handleOverrideDimensions(e.event)); + channel.onProcessResolvedShellLaunchConfig(e => this._ptys.get(e.id)?.handleResolvedShellLaunchConfig(e.event)); + channel.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); + channel.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); + + channel.onExecuteCommand(async e => { + const reqId = e.reqId; + const commandArgs = e.commandArgs.map(arg => revive(arg)); + try { + const result = await this._commandService.executeCommand(e.commandId, ...commandArgs); + channel!.sendCommandResult(reqId, false, result); + } catch (err) { + channel!.sendCommandResult(reqId, true, err); + } + }); + + // Attach pty host listeners + if (channel.onPtyHostExit) { + this._register(channel.onPtyHostExit(() => { + notificationService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`); + })); + } + let unresponsiveNotification: INotificationHandle | undefined; + if (channel.onPtyHostStart) { + this._register(channel.onPtyHostStart(() => { + this._logService.info(`ptyHost restarted`); + this._onPtyHostRestart.fire(); + unresponsiveNotification?.close(); + unresponsiveNotification = undefined; + this._isPtyHostUnresponsive = false; + })); + } + if (channel.onPtyHostUnresponsive) { + this._register(channel.onPtyHostUnresponsive(() => { + const choices: IPromptChoice[] = [{ + label: localize('restartPtyHost', "Restart pty host"), + run: () => channel.restartPtyHost!() + }]; + unresponsiveNotification = notificationService.prompt(Severity.Error, localize('nonResponsivePtyHost', "The connection to the terminal's pty host process is unresponsive, the terminals may stop working."), choices); + this._isPtyHostUnresponsive = true; + this._onPtyHostUnresponsive.fire(); + })); + } + if (channel.onPtyHostResponsive) { + this._register(channel.onPtyHostResponsive(() => { + if (!this._isPtyHostUnresponsive) { + return; + } + this._logService.info('The pty host became responsive again'); + unresponsiveNotification?.close(); + unresponsiveNotification = undefined; + this._isPtyHostUnresponsive = false; + this._onPtyHostResponsive.fire(); + })); + } } else { this._remoteTerminalChannel = null; } } - public async createRemoteTerminalProcess(instanceId: number, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise { + public async createProcess(shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise { if (!this._remoteTerminalChannel) { throw new Error(`Cannot create remote terminal when there is no remote!`); } - let isPreconnectionTerminal = false; - if (!this._hasConnectedToRemote) { - isPreconnectionTerminal = true; - this._remoteAgentService.getEnvironment().then(() => { - this._hasConnectedToRemote = true; - }); + // Fetch the environment to check shell permissions + const remoteEnv = await this._remoteAgentService.getEnvironment(); + if (!remoteEnv) { + // Extension host processes are only allowed in remote extension hosts currently + throw new Error('Could not fetch remote environment'); } - return new RemoteTerminalProcess(instanceId, shouldPersist, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, configHelper, isPreconnectionTerminal, this._remoteTerminalChannel, this._remoteAgentService, this._logService, this._commandService); + const shellLaunchConfigDto: IShellLaunchConfigDto = { + name: shellLaunchConfig.name, + executable: shellLaunchConfig.executable, + args: shellLaunchConfig.args, + cwd: shellLaunchConfig.cwd, + env: shellLaunchConfig.env + }; + const isWorkspaceShellAllowed = configHelper.checkWorkspaceShellPermissions(remoteEnv.os); + const result = await this._remoteTerminalChannel.createProcess( + shellLaunchConfigDto, + activeWorkspaceRootUri, + shouldPersist, + cols, + rows, + isWorkspaceShellAllowed, + ); + const pty = new RemotePty(result.persistentTerminalId, shouldPersist, this._remoteTerminalChannel, this._remoteAgentService, this._logService); + this._ptys.set(result.persistentTerminalId, pty); + return pty; } - public async listTerminals(isInitialization = false): Promise { - const terms = this._remoteTerminalChannel ? await this._remoteTerminalChannel.listTerminals(isInitialization) : []; + public async attachToProcess(id: number): Promise { + if (!this._remoteTerminalChannel) { + throw new Error(`Cannot create remote terminal when there is no remote!`); + } + + try { + await this._remoteTerminalChannel.attachToProcess(id); + const pty = new RemotePty(id, true, this._remoteTerminalChannel, this._remoteAgentService, this._logService); + this._ptys.set(id, pty); + return pty; + } catch (e) { + this._logService.trace(`Couldn't attach to process ${e.message}`); + } + return undefined; + } + + public async listProcesses(reduceGraceTime: boolean = false): Promise { + const terms = this._remoteTerminalChannel ? await this._remoteTerminalChannel.listProcesses(reduceGraceTime) : []; return terms.map(termDto => { return { id: termDto.id, @@ -86,227 +198,3 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal return this._remoteTerminalChannel.getTerminalLayoutInfo(); } } - -export class RemoteTerminalProcess extends Disposable implements ITerminalChildProcess { - - public readonly _onProcessData = this._register(new Emitter()); - public readonly onProcessData: Event = this._onProcessData.event; - private readonly _onProcessExit = this._register(new Emitter()); - public readonly onProcessExit: Event = this._onProcessExit.event; - public readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>()); - public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; } - private readonly _onProcessTitleChanged = this._register(new Emitter()); - public readonly onProcessTitleChanged: Event = this._onProcessTitleChanged.event; - private readonly _onProcessOverrideDimensions = this._register(new Emitter()); - public readonly onProcessOverrideDimensions: Event = this._onProcessOverrideDimensions.event; - private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter()); - public get onProcessResolvedShellLaunchConfig(): Event { return this._onProcessResolvedShellLaunchConfig.event; } - private readonly _onProcessShellTypeChanged = this._register(new Emitter()); - public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event; - - private _startBarrier: Barrier; - private _persistentProcessId: number; - public get id(): number { return this._persistentProcessId; } - - private _inReplay = false; - - constructor( - private readonly _instanceId: number, - readonly shouldPersist: boolean, - private readonly _shellLaunchConfig: IShellLaunchConfig, - private readonly _activeWorkspaceRootUri: URI | undefined, - private readonly _cols: number, - private readonly _rows: number, - private readonly _configHelper: ITerminalConfigHelper, - private readonly _isPreconnectionTerminal: boolean, - private readonly _remoteTerminalChannel: RemoteTerminalChannelClient, - private readonly _remoteAgentService: IRemoteAgentService, - private readonly _logService: ILogService, - private readonly _commandService: ICommandService, - ) { - super(); - this._startBarrier = new Barrier(); - this._persistentProcessId = 0; - - if (this._isPreconnectionTerminal) { - // Add a loading title only if this terminal is - // instantiated before a connection is up and running - setTimeout(() => this._onProcessTitleChanged.fire(nls.localize('terminal.integrated.starting', "Starting...")), 0); - } - } - - public async start(): Promise { - // Fetch the environment to check shell permissions - const env = await this._remoteAgentService.getEnvironment(); - if (!env) { - // Extension host processes are only allowed in remote extension hosts currently - throw new Error('Could not fetch remote environment'); - } - - if (!this._shellLaunchConfig.attachPersistentProcess) { - const isWorkspaceShellAllowed = this._configHelper.checkWorkspaceShellPermissions(env.os); - - const shellLaunchConfigDto: IShellLaunchConfigDto = { - name: this._shellLaunchConfig.name, - executable: this._shellLaunchConfig.executable, - args: this._shellLaunchConfig.args, - cwd: this._shellLaunchConfig.cwd, - env: this._shellLaunchConfig.env - }; - - this._logService.trace('Spawning remote agent process', { terminalId: this._instanceId, shellLaunchConfigDto }); - - const result = await this._remoteTerminalChannel.createTerminalProcess( - shellLaunchConfigDto, - this._activeWorkspaceRootUri, - this.shouldPersist, - this._cols, - this._rows, - isWorkspaceShellAllowed, - ); - - this._persistentProcessId = result.terminalId; - this.setupTerminalEventListener(); - this._onProcessResolvedShellLaunchConfig.fire(reviveIShellLaunchConfig(result.resolvedShellLaunchConfig)); - - const startResult = await this._remoteTerminalChannel.startTerminalProcess(this._persistentProcessId); - - if (typeof startResult !== 'undefined') { - // An error occurred - return startResult; - } - } else { - this._persistentProcessId = this._shellLaunchConfig.attachPersistentProcess.id; - this._onProcessReady.fire({ pid: this._shellLaunchConfig.attachPersistentProcess.pid, cwd: this._shellLaunchConfig.attachPersistentProcess.cwd }); - this.setupTerminalEventListener(); - - setTimeout(() => { - this._onProcessTitleChanged.fire(this._shellLaunchConfig.attachPersistentProcess!.title); - }, 0); - } - - this._startBarrier.open(); - return undefined; - } - - public shutdown(immediate: boolean): void { - this._startBarrier.wait().then(_ => { - this._remoteTerminalChannel.shutdownTerminalProcess(this._persistentProcessId, immediate); - }); - } - - public input(data: string): void { - if (this._inReplay) { - return; - } - - this._startBarrier.wait().then(_ => { - this._remoteTerminalChannel.sendInputToTerminalProcess(this._persistentProcessId, data); - }); - } - - private setupTerminalEventListener(): void { - this._register(this._remoteTerminalChannel.onTerminalProcessEvent(this._persistentProcessId)(event => { - switch (event.type) { - case 'ready': - return this._onProcessReady.fire({ pid: event.pid, cwd: event.cwd }); - case 'titleChanged': - return this._onProcessTitleChanged.fire(event.title); - case 'data': - return this._onProcessData.fire({ data: event.data, sync: false }); - case 'replay': { - try { - this._inReplay = true; - - for (const e of event.events) { - if (e.cols !== 0 || e.rows !== 0) { - // never override with 0x0 as that is a marker for an unknown initial size - this._onProcessOverrideDimensions.fire({ cols: e.cols, rows: e.rows, forceExactSize: true }); - } - this._onProcessData.fire({ data: e.data, sync: true }); - } - } finally { - this._inReplay = false; - } - - // remove size override - this._onProcessOverrideDimensions.fire(undefined); - - return; - } - case 'exit': - return this._onProcessExit.fire(event.exitCode); - case 'execCommand': - return this._execCommand(event); - case 'orphan?': { - this._remoteTerminalChannel.orphanQuestionReply(this._persistentProcessId); - return; - } - } - })); - } - - public resize(cols: number, rows: number): void { - if (this._inReplay) { - return; - } - this._startBarrier.wait().then(_ => { - - this._remoteTerminalChannel.resizeTerminalProcess(this._persistentProcessId, cols, rows); - }); - } - - public acknowledgeDataEvent(charCount: number): void { - // Support flow control for server spawned processes - if (this._inReplay) { - return; - } - - this._startBarrier.wait().then(_ => { - this._remoteTerminalChannel.sendCharCountToTerminalProcess(this._persistentProcessId, charCount); - }); - } - - public async getInitialCwd(): Promise { - await this._startBarrier.wait(); - return this._remoteTerminalChannel.getTerminalInitialCwd(this._persistentProcessId); - } - - public async getCwd(): Promise { - await this._startBarrier.wait(); - return this._remoteTerminalChannel.getTerminalCwd(this._persistentProcessId); - } - - /** - * TODO@roblourens I don't think this does anything useful in the EH and the value isn't used - */ - public async getLatency(): Promise { - return 0; - } - - private async _execCommand(event: IRemoteTerminalProcessExecCommandEvent): Promise { - const reqId = event.reqId; - const commandArgs = event.commandArgs.map(arg => revive(arg)); - try { - const result = await this._commandService.executeCommand(event.commandId, ...commandArgs); - this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._persistentProcessId, reqId, false, result); - } catch (err) { - this._remoteTerminalChannel.sendCommandResultToTerminalProcess(this._persistentProcessId, reqId, true, err); - } - } -} - -function reviveIShellLaunchConfig(dto: IShellLaunchConfigDto): IShellLaunchConfig { - return { - name: dto.name, - executable: dto.executable, - args: dto.args, - cwd: ( - (typeof dto.cwd === 'string' || typeof dto.cwd === 'undefined') - ? dto.cwd - : URI.revive(dto.cwd) - ), - env: dto.env, - hideFromUser: dto.hideFromUser - }; -} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index d6d386a16c2..242a5289199 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -9,7 +9,7 @@ import { IProcessEnvironment, Platform } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ITerminalTabLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; +import { IOffProcessTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalTabLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IAvailableShellsRequest, ICommandTracker, IDefaultShellAndArgsRequest, INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, LinuxDistro, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; @@ -181,14 +181,8 @@ export interface ITerminalService { isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean; } -export interface IRemoteTerminalService { - readonly _serviceBrand: undefined; - dispose(): void; - listTerminals(isInitialization?: boolean): Promise; - createRemoteTerminalProcess(instanceId: number, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper,): Promise; - - setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise; - getTerminalLayoutInfo(): Promise; +export interface IRemoteTerminalService extends IOffProcessTerminalService { + createProcess(shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise; } /** diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index e62b748f49b..13c42e42de7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -24,9 +24,9 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPickOptions, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { ILocalTerminalService } from 'vs/platform/terminal/common/terminal'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; -import { RemoteNameContext } from 'vs/workbench/browser/contextkeys'; import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; import { Direction, IRemoteTerminalService, ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; @@ -35,6 +35,7 @@ import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/comm import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export const switchTerminalActionViewItemSeparator = '─────────'; export const selectDefaultShellTitle = localize('workbench.action.terminal.selectDefaultShell', "Select Default Shell"); @@ -662,19 +663,16 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.ATTACH_TO_REMOTE_TERMINAL, title: { value: localize('workbench.action.terminal.attachToRemote', "Attach to Session"), original: 'Attach to Session' }, f1: true, - category, - keybinding: { - when: RemoteNameContext.notEqualsTo(''), - weight: KeybindingWeight.WorkbenchContrib - } + category }); } async run(accessor: ServicesAccessor) { const quickInputService = accessor.get(IQuickInputService); - const remoteTerminalService = accessor.get(IRemoteTerminalService); const terminalService = accessor.get(ITerminalService); const labelService = accessor.get(ILabelService); - const remoteTerms = await remoteTerminalService.listTerminals(); + const remoteAgentService = accessor.get(IRemoteAgentService); + const offProcTerminalService = remoteAgentService.getConnection() ? accessor.get(IRemoteTerminalService) : accessor.get(ILocalTerminalService); + const remoteTerms = await offProcTerminalService.listProcesses(); const unattachedTerms = remoteTerms.filter(term => !terminalService.isAttachedToTerminal(term)); const items = unattachedTerms.map(term => { const cwdLabel = labelService.getUriLabel(URI.file(term.cwd)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index c8cb4eb5eef..6b363b22bb8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -971,6 +971,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private _createProcess(): void { + if (this._isDisposed) { + return; + } this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows, this._accessibilityService.isScreenReaderOptimized()).then(error => { if (error) { this._onProcessExit(error); @@ -1563,7 +1566,15 @@ 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.isExtensionCustomPtyTerminal && !this._shellLaunchConfig.isExtensionOwnedTerminal && !this._shellLaunchConfig.attachPersistentProcess) { + if ( + info.requiresAction && + this._configHelper.config.environmentChangesRelaunch && + !this._processManager.hasWrittenData && + !this._shellLaunchConfig.isFeatureTerminal && + !this._shellLaunchConfig.isExtensionCustomPtyTerminal + && !this._shellLaunchConfig.isExtensionOwnedTerminal && + !this._shellLaunchConfig.attachPersistentProcess + ) { this.relaunch(); return; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index e5fe9253ac4..ac123a8626d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -27,7 +27,7 @@ import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } fr import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { URI } from 'vs/base/common/uri'; import { IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, TerminalShellType, ILocalTerminalService } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, TerminalShellType, ILocalTerminalService, IOffProcessTerminalService } from 'vs/platform/terminal/common/terminal'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -59,6 +59,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce public userHome: string | undefined; public isDisconnected: boolean = false; + private _isDisposed: boolean = false; private _process: ITerminalChildProcess | null = null; private _processType: ProcessType = ProcessType.Process; private _preLaunchInputQueue: string[] = []; @@ -137,6 +138,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } public dispose(immediate: boolean = false): void { + this._isDisposed = true; if (this._process) { // If the process was still connected this dispose came from // within VS Code, not the process, so mark the process as @@ -195,14 +197,25 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce await this._setupEnvVariableInfo(activeWorkspaceRootUri, shellLaunchConfig); const shouldPersist = !shellLaunchConfig.isFeatureTerminal && this._configHelper.config.enablePersistentSessions; - this._process = await this._remoteTerminalService.createRemoteTerminalProcess(this._instanceId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, shouldPersist, this._configHelper); + if (shellLaunchConfig.attachPersistentProcess) { + const result = await this._remoteTerminalService.attachToProcess(shellLaunchConfig.attachPersistentProcess.id); + if (result) { + this._process = result; + } else { + this._logService.trace(`Attach to process failed for terminal ${shellLaunchConfig.attachPersistentProcess}`); + return undefined; + } + } else { + this._process = await this._remoteTerminalService.createProcess(shellLaunchConfig, activeWorkspaceRootUri, cols, rows, shouldPersist, this._configHelper); + } + if (!this._isDisposed) { + this._setupPtyHostListeners(this._remoteTerminalService); + } } else { if (!this._localTerminalService) { this._logService.trace(`Tried to launch a local terminal which is not supported in this window`); return undefined; } - // Flow control is not needed for ptys hosted in the same process (ie. the electron - // renderer). if (shellLaunchConfig.attachPersistentProcess) { const result = await this._localTerminalService.attachToProcess(shellLaunchConfig.attachPersistentProcess.id); if (result) { @@ -214,9 +227,18 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } else { this._process = await this._launchLocalProcess(this._localTerminalService, shellLaunchConfig, cols, rows, this.userHome, isScreenReaderModeEnabled); } + if (!this._isDisposed) { + this._setupPtyHostListeners(this._localTerminalService); + } } } + // If the process was disposed during its creation, shut it down and return failure + if (this._isDisposed) { + this._process.shutdown(false); + return undefined; + } + this.processState = ProcessState.LAUNCHING; this._process.onProcessData(ev => { @@ -338,13 +360,17 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const useConpty = this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled; const shouldPersist = this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isFeatureTerminal; + return await localTerminalService.createProcess(shellLaunchConfig, initialCwd, cols, rows, env, useConpty, shouldPersist); + } + + private _setupPtyHostListeners(offProcessTerminalService: IOffProcessTerminalService) { // Mark the process as disconnected is the pty host is unresponsive, the responsive event // will fire only when the pty host was already unresponsive - this._register(localTerminalService.onPtyHostUnresponsive(() => { + this._register(offProcessTerminalService.onPtyHostUnresponsive(() => { this.isDisconnected = true; this._onPtyDisconnect.fire(); })); - this._ptyResponsiveListener = localTerminalService.onPtyHostResponsive(() => { + this._ptyResponsiveListener = offProcessTerminalService.onPtyHostResponsive(() => { this.isDisconnected = false; this._onPtyReconnect.fire(); }); @@ -352,12 +378,15 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // When the pty host restarts, reconnect is no longer possible so dispose the responsive // listener - this._register(localTerminalService.onPtyHostRestart(() => { + this._register(offProcessTerminalService.onPtyHostRestart(() => { + // When the pty host restarts, reconnect is no longer possible + if (!this.isDisconnected) { + this.isDisconnected = true; + this._onPtyDisconnect.fire(); + } this._ptyResponsiveListener?.dispose(); this._ptyResponsiveListener = undefined; })); - - return await localTerminalService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, useConpty, shouldPersist); } public setDimensions(cols: number, rows: number): void { diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index 43c7546c7be..ec801f006d5 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -18,9 +18,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Schemas } from 'vs/base/common/network'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalShellType } 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'; +import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; export const REMOTE_TERMINAL_CHANNEL_NAME = 'remoteterminal'; @@ -83,121 +83,55 @@ export interface ICreateTerminalProcessArguments { } export interface ICreateTerminalProcessResult { - terminalId: number; + persistentTerminalId: number; resolvedShellLaunchConfig: IShellLaunchConfigDto; } -export interface IStartTerminalProcessArguments { - id: number; -} - -export interface ISendInputToTerminalProcessArguments { - id: number; - data: string; -} - -export interface IShutdownTerminalProcessArguments { - id: number; - immediate: boolean; -} - -export interface IResizeTerminalProcessArguments { - id: number; - cols: number; - rows: number; -} - -export interface IGetTerminalInitialCwdArguments { - id: number; -} - -export interface IGetTerminalCwdArguments { - id: number; -} - -export interface ISendCommandResultToTerminalProcessArguments { - id: number; - reqId: number; - isError: boolean; - payload: any; -} - -export interface IOrphanQuestionReplyArgs { - id: number; -} - -export interface IListTerminalsArgs { - isInitialization: boolean; -} - -export interface IRemoteTerminalDescriptionDto { - id: number; - pid: number; - title: string; - cwd: string; - workspaceId: string; - workspaceName: string; - isOrphan: boolean; -} - -export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; - -export interface ITriggerTerminalDataReplayArguments { - id: number; -} - -export interface ISendCharCountToTerminalProcessArguments { - id: number; - charCount: number; -} - -export interface IRemoteTerminalProcessReadyEvent { - type: 'ready'; - pid: number; - cwd: string; -} -export interface IRemoteTerminalProcessTitleChangedEvent { - type: 'titleChanged'; - title: string; -} -export interface IRemoteTerminalProcessDataEvent { - type: 'data'; - data: string; -} -export interface ReplayEntry { cols: number; rows: number; data: string; } -export interface IRemoteTerminalProcessReplayEvent { - type: 'replay'; - events: ReplayEntry[]; -} -export interface IRemoteTerminalProcessExitEvent { - type: 'exit'; - exitCode: number | undefined; -} -export interface IRemoteTerminalProcessExecCommandEvent { - type: 'execCommand'; - reqId: number; - commandId: string; - commandArgs: any[]; -} -export interface IRemoteTerminalProcessOrphanQuestionEvent { - type: 'orphan?'; -} -export type IRemoteTerminalProcessEvent = ( - IRemoteTerminalProcessReadyEvent - | IRemoteTerminalProcessTitleChangedEvent - | IRemoteTerminalProcessDataEvent - | IRemoteTerminalProcessReplayEvent - | IRemoteTerminalProcessExitEvent - | IRemoteTerminalProcessExecCommandEvent - | IRemoteTerminalProcessOrphanQuestionEvent -); - -export interface IOnTerminalProcessEventArguments { - id: number; -} - export class RemoteTerminalChannelClient { + public get onPtyHostExit(): Event { + return this._channel.listen('$onPtyHostExitEvent'); + } + public get onPtyHostStart(): Event { + return this._channel.listen('$onPtyHostStartEvent'); + } + public get onPtyHostUnresponsive(): Event { + return this._channel.listen('$onPtyHostUnresponsiveEvent'); + } + public get onPtyHostResponsive(): Event { + return this._channel.listen('$onPtyHostResponsiveEvent'); + } + public get onProcessData(): Event<{ id: number, event: IProcessDataEvent | string }> { + return this._channel.listen<{ id: number, event: IProcessDataEvent | string }>('$onProcessDataEvent'); + } + public get onProcessExit(): Event<{ id: number, event: number | undefined }> { + return this._channel.listen<{ id: number, event: number | undefined }>('$onProcessExitEvent'); + } + public get onProcessReady(): Event<{ id: number, event: { pid: number, cwd: string } }> { + return this._channel.listen<{ id: number, event: { pid: number, cwd: string } }>('$onProcessReadyEvent'); + } + public get onProcessReplay(): Event<{ id: number, event: IPtyHostProcessReplayEvent }> { + return this._channel.listen<{ id: number, event: IPtyHostProcessReplayEvent }>('$onProcessReplayEvent'); + } + public get onProcessTitleChanged(): Event<{ id: number, event: string }> { + return this._channel.listen<{ id: number, event: string }>('$onProcessTitleChangedEvent'); + } + public get onProcessShellTypeChanged(): Event<{ id: number, event: TerminalShellType | undefined }> { + return this._channel.listen<{ id: number, event: TerminalShellType | undefined }>('$onProcessShellTypeChangedEvent'); + } + public get onProcessOverrideDimensions(): Event<{ id: number, event: ITerminalDimensionsOverride | undefined }> { + return this._channel.listen<{ id: number, event: ITerminalDimensionsOverride | undefined }>('$onProcessOverrideDimensionsEvent'); + } + public get onProcessResolvedShellLaunchConfig(): Event<{ id: number, event: IShellLaunchConfig }> { + return this._channel.listen<{ id: number, event: IShellLaunchConfig }>('$onProcessResolvedShellLaunchConfigEvent'); + } + public get onProcessOrphanQuestion(): Event<{ id: number }> { + return this._channel.listen<{ id: number }>('$onProcessOrphanQuestion'); + } + public get onExecuteCommand(): Event<{ reqId: number, commandId: string, commandArgs: any[] }> { + return this._channel.listen<{ reqId: number, commandId: string, commandArgs: any[] }>('$onExecuteCommand'); + } + constructor( private readonly _remoteAuthority: string, private readonly _channel: IChannel, @@ -220,7 +154,11 @@ export class RemoteTerminalChannelClient { }; } - public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { + restartPtyHost(): Promise { + return this._channel.call('$restartPtyHost', []); + } + + public async createProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { // Be sure to first wait for the remote configuration await this._configurationService.whenRemoteConfigurationLoaded(); @@ -297,87 +235,44 @@ export class RemoteTerminalChannelClient { isWorkspaceShellAllowed, resolverEnv }; - return await this._channel.call('$createTerminalProcess', args); + return await this._channel.call('$createProcess', args); } - public async startTerminalProcess(terminalId: number): Promise { - const args: IStartTerminalProcessArguments = { - id: terminalId - }; - return this._channel.call('$startTerminalProcess', args); + public attachToProcess(id: number): Promise { + return this._channel.call('$attachToProcess', [id]); } - public onTerminalProcessEvent(terminalId: number): Event { - const args: IOnTerminalProcessEventArguments = { - id: terminalId - }; - return this._channel.listen('$onTerminalProcessEvent', args); + public listProcesses(reduceGraceTime: boolean): Promise { + return this._channel.call('$listProcesses', [reduceGraceTime]); } - public sendInputToTerminalProcess(id: number, data: string): Promise { - const args: ISendInputToTerminalProcessArguments = { - id, data - }; - return this._channel.call('$sendInputToTerminalProcess', args); + public start(id: number): Promise { + return this._channel.call('$start', [id]); } - - public sendCharCountToTerminalProcess(id: number, charCount: number): Promise { - const args: ISendCharCountToTerminalProcessArguments = { - id, charCount - }; - return this._channel.call('$sendCharCountToTerminalProcess', args); + public input(id: number, data: string): Promise { + return this._channel.call('$input', [id, data]); } - - public shutdownTerminalProcess(id: number, immediate: boolean): Promise { - const args: IShutdownTerminalProcessArguments = { - id, immediate - }; - return this._channel.call('$shutdownTerminalProcess', args); + public acknowledgeDataEvent(id: number, charCount: number): Promise { + return this._channel.call('$acknowledgeDataEvent', [id, charCount]); } - - public resizeTerminalProcess(id: number, cols: number, rows: number): Promise { - const args: IResizeTerminalProcessArguments = { - id, cols, rows - }; - return this._channel.call('$resizeTerminalProcess', args); + public shutdown(id: number, immediate: boolean): Promise { + return this._channel.call('$shutdown', [id, immediate]); } - - public getTerminalInitialCwd(id: number): Promise { - const args: IGetTerminalInitialCwdArguments = { - id - }; - return this._channel.call('$getTerminalInitialCwd', args); + public resize(id: number, cols: number, rows: number): Promise { + return this._channel.call('$resize', [id, cols, rows]); } - - public getTerminalCwd(id: number): Promise { - const args: IGetTerminalCwdArguments = { - id - }; - return this._channel.call('$getTerminalCwd', args); + public getInitialCwd(id: number): Promise { + return this._channel.call('$getInitialCwd', [id]); } - - public sendCommandResultToTerminalProcess(id: number, reqId: number, isError: boolean, payload: any): Promise { - const args: ISendCommandResultToTerminalProcessArguments = { - id, - reqId, - isError, - payload - }; - return this._channel.call('$sendCommandResultToTerminalProcess', args); + public getCwd(id: number): Promise { + return this._channel.call('$getCwd', [id]); } - public orphanQuestionReply(id: number): Promise { - const args: IOrphanQuestionReplyArgs = { - id - }; - return this._channel.call('$orphanQuestionReply', args); + return this._channel.call('$orphanQuestionReply', [id]); } - public listTerminals(isInitialization: boolean): Promise { - const args: IListTerminalsArgs = { - isInitialization - }; - return this._channel.call('$listTerminals', args); + public sendCommandResult(reqId: number, isError: boolean, payload: any): Promise { + return this._channel.call('$sendCommandResult', [reqId, isError, payload]); } public setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index dff256e9500..004f39ba065 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -130,6 +130,7 @@ export interface ITerminalConfiguration { windows: { [key: string]: string }; }; environmentChangesIndicator: 'off' | 'on' | 'warnonly'; + environmentChangesRelaunch: boolean; showExitAlert: boolean; splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; windowsEnableConpty: boolean; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index d275d0afa00..b4ddfa8a6d4 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -311,6 +311,11 @@ export const terminalConfiguration: IConfigurationNode = { ], default: 'warnonly' }, + 'terminal.integrated.environmentChangesRelaunch': { + markdownDescription: localize('terminal.integrated.environmentChangesRelaunch', "Whether to relaunch terminals automatically if extension want to contribute to their environment and have not been interacted with yet."), + type: 'boolean', + default: true + }, 'terminal.integrated.showExitAlert': { description: localize('terminal.integrated.showExitAlert', "Controls whether to show the alert \"The terminal process terminated with exit code\" when exit code is non-zero."), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts index b26c7d38d50..bd840ca32d6 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts @@ -13,7 +13,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationHandle, INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; -import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { LocalPty } from 'vs/workbench/contrib/terminal/electron-sandbox/localPty'; @@ -24,8 +24,6 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe private readonly _ptys: Map = new Map(); private _isPtyHostUnresponsive: boolean = false; - private readonly _onPtyHostExit = this._register(new Emitter()); - readonly onPtyHostExit = this._onPtyHostExit.event; private readonly _onPtyHostUnresponsive = this._register(new Emitter()); readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event; private readonly _onPtyHostResponsive = this._register(new Emitter()); @@ -61,7 +59,6 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe // Attach pty host listeners if (this._localPtyService.onPtyHostExit) { this._register(this._localPtyService.onPtyHostExit(() => { - this._onPtyHostExit.fire(); notificationService.error(`The terminal's pty host process exited, the connection to all terminal processes was lost`); })); } @@ -100,7 +97,7 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe } } - public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise { + public async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise { const id = await this._localPtyService.createProcess(shellLaunchConfig, cwd, cols, rows, env, processEnv as IProcessEnvironment, windowsEnableConpty, shouldPersist, this._getWorkspaceId(), this._getWorkspaceName()); const pty = this._instantiationService.createInstance(LocalPty, id, shouldPersist); this._ptys.set(id, pty); @@ -119,12 +116,16 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe return undefined; } - public setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): void { + public async listProcesses(reduceGraceTime: boolean): Promise { + return this._localPtyService.listProcesses(reduceGraceTime); + } + + public async setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise { const args: ISetTerminalLayoutInfoArgs = { workspaceId: this._getWorkspaceId(), tabs: layoutInfo ? layoutInfo.tabs : [] }; - this._localPtyService.setTerminalLayoutInfo(args); + await this._localPtyService.setTerminalLayoutInfo(args); } public async getTerminalLayoutInfo(): Promise { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 28a8f78cc1b..188602cc4b3 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -127,7 +127,7 @@ import { IEnterWorkspaceResult, IRecent, IRecentlyOpened, IWorkspaceFolderCreati import { IWorkspaceTrustService } from 'vs/platform/workspace/common/workspaceTrust'; import { TestWorkspaceTrustService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalsLayoutInfo, ITerminalsLayoutInfoById } from 'vs/platform/terminal/common/terminal'; -import { ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { @@ -1485,12 +1485,12 @@ export class TestLocalTerminalService implements ILocalTerminalService { onPtyHostResponsive = Event.None; onPtyHostRestart = Event.None; - async createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise { + async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise { return new TestTerminalChildProcess(shouldPersist); } - async attachToProcess(id: number): Promise { throw new Error('Method not implemented.'); } - setTerminalLayoutInfo(argsOrLayout?: ISetTerminalLayoutInfoArgs | ITerminalsLayoutInfoById): void { throw new Error('Method not implemented.'); } + async listProcesses(reduceGraceTime: boolean): Promise { throw new Error('Method not implemented.'); } + async setTerminalLayoutInfo(argsOrLayout?: ISetTerminalLayoutInfoArgs | ITerminalsLayoutInfoById) { throw new Error('Method not implemented.'); } async getTerminalLayoutInfo(): Promise { throw new Error('Method not implemented.'); } }