Merge pull request #118252 from microsoft/tyriar/116467_2

Consolidate local and remote terminal implementations
This commit is contained in:
Daniel Imms 2021-03-09 07:45:49 -08:00 committed by GitHub
commit 2c11dafcc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 551 additions and 491 deletions

View file

@ -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', () => {

View file

@ -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();
});

View file

@ -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"
}
}

View file

@ -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"

View file

@ -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);
}

View file

@ -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<void>;
/**
* 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<void>;
createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise<ITerminalChildProcess>;
attachToProcess(id: number): Promise<ITerminalChildProcess | undefined>;
setTerminalLayoutInfo(args?: ISetTerminalLayoutInfoArgs): void;
setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): void;
listProcesses(reduceGraceTime?: boolean): Promise<IProcessDetails[]>;
setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise<void>;
getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined>;
}
export const ILocalTerminalService = createDecorator<ILocalTerminalService>('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<ITerminalChildProcess>;
}
export const IPtyService = createDecorator<IPtyService>('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<void>;
shutdownAll?(): Promise<void>;
@ -139,6 +140,13 @@ export interface IPtyService {
attachToProcess(id: number): Promise<void>;
detachFromProcess(id: number): Promise<void>;
/**
* 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<IProcessDetails[]>;
start(id: number): Promise<ITerminalLaunchError | undefined>;
shutdown(id: number, immediate: boolean): Promise<void>;
input(id: number, data: string): Promise<void>;
@ -147,8 +155,10 @@ export interface IPtyService {
getCwd(id: number): Promise<string>;
getLatency(id: number): Promise<number>;
acknowledgeDataEvent(id: number, charCount: number): Promise<void>;
/** Confirm the process is _not_ an orphan. */
orphanQuestionReply(id: number): Promise<void>;
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void;
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void>;
getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined>;
}
@ -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...
*/

View file

@ -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<IPtyHostDescriptionDto>;
export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IProcessDetails>;
export interface ReplayEntry { cols: number; rows: number; data: string; }
export interface IPtyHostProcessReplayEvent {

View file

@ -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<void> {
return this._proxy.detachFromProcess(id);
}
listProcesses(reduceGraceTime: boolean): Promise<IProcessDetails[]> {
return this._proxy.listProcesses(reduceGraceTime);
}
start(id: number): Promise<ITerminalLaunchError | undefined> {
return this._proxy.start(id);
@ -169,7 +179,11 @@ export class LocalPtyService extends Disposable implements IPtyService {
getLatency(id: number): Promise<number> {
return this._proxy.getLatency(id);
}
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void {
orphanQuestionReply(id: number): Promise<void> {
return this._proxy.orphanQuestionReply(id);
}
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
return this._proxy.setTerminalLayoutInfo(args);
}
async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined> {

View file

@ -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<IProcessDetails[]> {
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<ITerminalLaunchError | undefined> {
return this._throwIfNoPty(id).start();
}
@ -134,6 +152,9 @@ export class PtyService extends Disposable implements IPtyService {
async getLatency(id: number): Promise<number> {
return 0;
}
async orphanQuestionReply(id: number): Promise<void> {
return this._throwIfNoPty(id).orphanQuestionReply();
}
async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
this._workspaceLayoutInfos.set(args.workspaceId, args);
@ -153,7 +174,7 @@ export class PtyService extends Disposable implements IPtyService {
private async _expandTerminalTab(tab: ITerminalTabLayoutInfoById): Promise<ITerminalTabLayoutInfoDto> {
const expandedTerminals = (await Promise.all(tab.terminals.map(t => this._expandTerminalInstance(t))));
const filtered = expandedTerminals.filter(term => term.terminal !== null) as IRawTerminalInstanceLayoutInfo<IPtyHostDescriptionDto>[];
const filtered = expandedTerminals.filter(term => term.terminal !== null) as IRawTerminalInstanceLayoutInfo<IProcessDetails>[];
return {
isActive: tab.isActive,
activePersistentProcessId: tab.activePersistentProcessId,
@ -161,12 +182,12 @@ export class PtyService extends Disposable implements IPtyService {
};
}
private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise<IRawTerminalInstanceLayoutInfo<IPtyHostDescriptionDto | null>> {
private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise<IRawTerminalInstanceLayoutInfo<IProcessDetails | null>> {
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<IPtyHostDescriptionDto> {
private async _buildProcessDetails(id: number, persistentProcess: PersistentTerminalProcess): Promise<IProcessDetails> {
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<IProcessDataEvent>());
readonly onProcessData = this._onProcessData.event;
private readonly _onProcessOrphanQuestion = this._register(new Emitter<void>());
readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
private _inReplay = false;
@ -363,7 +386,7 @@ export class PersistentTerminalProcess extends Disposable {
this._pendingCommands.delete(reqId);
}
async orphanQuestionReply(): Promise<void> {
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<boolean> {
// 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();

View file

@ -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<string | IProcessDataEvent>());
public readonly onProcessData: Event<string | IProcessDataEvent> = this._onProcessData.event;
private readonly _onProcessExit = this._register(new Emitter<number | undefined>());
public readonly onProcessExit: Event<number | undefined> = 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<string>());
public readonly onProcessTitleChanged: Event<string> = this._onProcessTitleChanged.event;
private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType | undefined>());
public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>());
public readonly onProcessOverrideDimensions: Event<ITerminalDimensionsOverride | undefined> = this._onProcessOverrideDimensions.event;
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>());
public get onProcessResolvedShellLaunchConfig(): Event<IShellLaunchConfig> { 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<ITerminalLaunchError | undefined> {
// 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<string> {
await this._startBarrier.wait();
return this._remoteTerminalChannel.getInitialCwd(this._id);
}
public async getCwd(): Promise<string> {
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<number> {
return 0;
}
}

View file

@ -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<number, RemotePty> = new Map();
private readonly _remoteTerminalChannel: RemoteTerminalChannelClient | null;
private _hasConnectedToRemote = false;
private _isPtyHostUnresponsive: boolean = false;
private readonly _onPtyHostUnresponsive = this._register(new Emitter<void>());
readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event;
private readonly _onPtyHostResponsive = this._register(new Emitter<void>());
readonly onPtyHostResponsive = this._onPtyHostResponsive.event;
private readonly _onPtyHostRestart = this._register(new Emitter<void>());
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<ITerminalChildProcess> {
public async createProcess(shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise<ITerminalChildProcess> {
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<IRemoteTerminalAttachTarget[]> {
const terms = this._remoteTerminalChannel ? await this._remoteTerminalChannel.listTerminals(isInitialization) : [];
public async attachToProcess(id: number): Promise<ITerminalChildProcess | undefined> {
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<IRemoteTerminalAttachTarget[]> {
const terms = this._remoteTerminalChannel ? await this._remoteTerminalChannel.listProcesses(reduceGraceTime) : [];
return terms.map(termDto => {
return <IRemoteTerminalAttachTarget>{
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<IProcessDataEvent>());
public readonly onProcessData: Event<IProcessDataEvent> = this._onProcessData.event;
private readonly _onProcessExit = this._register(new Emitter<number | undefined>());
public readonly onProcessExit: Event<number | undefined> = 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<string>());
public readonly onProcessTitleChanged: Event<string> = this._onProcessTitleChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>());
public readonly onProcessOverrideDimensions: Event<ITerminalDimensionsOverride | undefined> = this._onProcessOverrideDimensions.event;
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>());
public get onProcessResolvedShellLaunchConfig(): Event<IShellLaunchConfig> { return this._onProcessResolvedShellLaunchConfig.event; }
private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType | undefined>());
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<ITerminalLaunchError | undefined> {
// 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<string> {
await this._startBarrier.wait();
return this._remoteTerminalChannel.getTerminalInitialCwd(this._persistentProcessId);
}
public async getCwd(): Promise<string> {
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<number> {
return 0;
}
private async _execCommand(event: IRemoteTerminalProcessExecCommandEvent): Promise<void> {
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
};
}

View file

@ -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<IRemoteTerminalAttachTarget[]>;
createRemoteTerminalProcess(instanceId: number, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper,): Promise<ITerminalChildProcess>;
setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise<void>;
getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined>;
export interface IRemoteTerminalService extends IOffProcessTerminalService {
createProcess(shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI | undefined, cols: number, rows: number, shouldPersist: boolean, configHelper: ITerminalConfigHelper): Promise<ITerminalChildProcess>;
}
/**

View file

@ -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));

View file

@ -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;
}

View file

@ -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 {

View file

@ -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<IRemoteTerminalDescriptionDto>;
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<void> {
return this._channel.listen<void>('$onPtyHostExitEvent');
}
public get onPtyHostStart(): Event<void> {
return this._channel.listen<void>('$onPtyHostStartEvent');
}
public get onPtyHostUnresponsive(): Event<void> {
return this._channel.listen<void>('$onPtyHostUnresponsiveEvent');
}
public get onPtyHostResponsive(): Event<void> {
return this._channel.listen<void>('$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<ICreateTerminalProcessResult> {
restartPtyHost(): Promise<void> {
return this._channel.call('$restartPtyHost', []);
}
public async createProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ICreateTerminalProcessResult> {
// 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<ICreateTerminalProcessResult>('$createTerminalProcess', args);
return await this._channel.call<ICreateTerminalProcessResult>('$createProcess', args);
}
public async startTerminalProcess(terminalId: number): Promise<ITerminalLaunchError | void> {
const args: IStartTerminalProcessArguments = {
id: terminalId
};
return this._channel.call<ITerminalLaunchError | void>('$startTerminalProcess', args);
public attachToProcess(id: number): Promise<void> {
return this._channel.call('$attachToProcess', [id]);
}
public onTerminalProcessEvent(terminalId: number): Event<IRemoteTerminalProcessEvent> {
const args: IOnTerminalProcessEventArguments = {
id: terminalId
};
return this._channel.listen<IRemoteTerminalProcessEvent>('$onTerminalProcessEvent', args);
public listProcesses(reduceGraceTime: boolean): Promise<IProcessDetails[]> {
return this._channel.call('$listProcesses', [reduceGraceTime]);
}
public sendInputToTerminalProcess(id: number, data: string): Promise<void> {
const args: ISendInputToTerminalProcessArguments = {
id, data
};
return this._channel.call<void>('$sendInputToTerminalProcess', args);
public start(id: number): Promise<ITerminalLaunchError | void> {
return this._channel.call('$start', [id]);
}
public sendCharCountToTerminalProcess(id: number, charCount: number): Promise<void> {
const args: ISendCharCountToTerminalProcessArguments = {
id, charCount
};
return this._channel.call<void>('$sendCharCountToTerminalProcess', args);
public input(id: number, data: string): Promise<void> {
return this._channel.call('$input', [id, data]);
}
public shutdownTerminalProcess(id: number, immediate: boolean): Promise<void> {
const args: IShutdownTerminalProcessArguments = {
id, immediate
};
return this._channel.call<void>('$shutdownTerminalProcess', args);
public acknowledgeDataEvent(id: number, charCount: number): Promise<void> {
return this._channel.call('$acknowledgeDataEvent', [id, charCount]);
}
public resizeTerminalProcess(id: number, cols: number, rows: number): Promise<void> {
const args: IResizeTerminalProcessArguments = {
id, cols, rows
};
return this._channel.call<void>('$resizeTerminalProcess', args);
public shutdown(id: number, immediate: boolean): Promise<void> {
return this._channel.call('$shutdown', [id, immediate]);
}
public getTerminalInitialCwd(id: number): Promise<string> {
const args: IGetTerminalInitialCwdArguments = {
id
};
return this._channel.call<string>('$getTerminalInitialCwd', args);
public resize(id: number, cols: number, rows: number): Promise<void> {
return this._channel.call('$resize', [id, cols, rows]);
}
public getTerminalCwd(id: number): Promise<string> {
const args: IGetTerminalCwdArguments = {
id
};
return this._channel.call<string>('$getTerminalCwd', args);
public getInitialCwd(id: number): Promise<string> {
return this._channel.call('$getInitialCwd', [id]);
}
public sendCommandResultToTerminalProcess(id: number, reqId: number, isError: boolean, payload: any): Promise<void> {
const args: ISendCommandResultToTerminalProcessArguments = {
id,
reqId,
isError,
payload
};
return this._channel.call<void>('$sendCommandResultToTerminalProcess', args);
public getCwd(id: number): Promise<string> {
return this._channel.call('$getCwd', [id]);
}
public orphanQuestionReply(id: number): Promise<void> {
const args: IOrphanQuestionReplyArgs = {
id
};
return this._channel.call<void>('$orphanQuestionReply', args);
return this._channel.call('$orphanQuestionReply', [id]);
}
public listTerminals(isInitialization: boolean): Promise<IRemoteTerminalDescriptionDto[]> {
const args: IListTerminalsArgs = {
isInitialization
};
return this._channel.call<IRemoteTerminalDescriptionDto[]>('$listTerminals', args);
public sendCommandResult(reqId: number, isError: boolean, payload: any): Promise<void> {
return this._channel.call<void>('$sendCommandResult', [reqId, isError, payload]);
}
public setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise<void> {

View file

@ -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;

View file

@ -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',

View file

@ -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<number, LocalPty> = new Map();
private _isPtyHostUnresponsive: boolean = false;
private readonly _onPtyHostExit = this._register(new Emitter<void>());
readonly onPtyHostExit = this._onPtyHostExit.event;
private readonly _onPtyHostUnresponsive = this._register(new Emitter<void>());
readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event;
private readonly _onPtyHostResponsive = this._register(new Emitter<void>());
@ -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<ITerminalChildProcess> {
public async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise<ITerminalChildProcess> {
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<IProcessDetails[]> {
return this._localPtyService.listProcesses(reduceGraceTime);
}
public async setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise<void> {
const args: ISetTerminalLayoutInfoArgs = {
workspaceId: this._getWorkspaceId(),
tabs: layoutInfo ? layoutInfo.tabs : []
};
this._localPtyService.setTerminalLayoutInfo(args);
await this._localPtyService.setTerminalLayoutInfo(args);
}
public async getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined> {

View file

@ -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<ITerminalChildProcess> {
async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise<ITerminalChildProcess> {
return new TestTerminalChildProcess(shouldPersist);
}
async attachToProcess(id: number): Promise<ITerminalChildProcess | undefined> { throw new Error('Method not implemented.'); }
setTerminalLayoutInfo(argsOrLayout?: ISetTerminalLayoutInfoArgs | ITerminalsLayoutInfoById): void { throw new Error('Method not implemented.'); }
async listProcesses(reduceGraceTime: boolean): Promise<IProcessDetails[]> { throw new Error('Method not implemented.'); }
async setTerminalLayoutInfo(argsOrLayout?: ISetTerminalLayoutInfoArgs | ITerminalsLayoutInfoById) { throw new Error('Method not implemented.'); }
async getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined> { throw new Error('Method not implemented.'); }
}