chore(vscode): update to 1.55.2

This commit is contained in:
Akash Satheesan
2021-04-09 11:32:27 +05:30
1102 changed files with 39988 additions and 23544 deletions

View File

@@ -3,11 +3,19 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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',
PowerShell = 'pwsh',
Wsl = 'wsl',
GitBash = 'gitbash'
}
export type TerminalShellType = WindowsShellType | undefined;
export interface IRawTerminalInstanceLayoutInfo<T> {
relativeSize: number;
terminal: T;
@@ -17,7 +25,7 @@ export type ITerminalInstanceLayoutInfo = IRawTerminalInstanceLayoutInfo<IPtyHos
export interface IRawTerminalTabLayoutInfo<T> {
isActive: boolean;
activePersistentTerminalId: number | undefined;
activePersistentProcessId: number | undefined;
terminals: IRawTerminalInstanceLayoutInfo<T>[];
}
@@ -65,6 +73,37 @@ export enum TerminalIpcChannels {
Heartbeat = 'heartbeat'
}
export interface IOffProcessTerminalService {
readonly _serviceBrand: undefined;
/**
* 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.
*/
onPtyHostUnresponsive: Event<void>;
/**
* Fired when the ptyHost process becomes responsive after being non-responsive. Allowing
* previously disconnected terminals to reconnect.
*/
onPtyHostResponsive: Event<void>;
/**
* Fired when the ptyHost has been restarted, this is used as a signal for listening terminals
* that its pty has been lost and will remain disconnected.
*/
onPtyHostRestart: Event<void>;
attachToProcess(id: number): Promise<ITerminalChildProcess | undefined>;
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 {
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;
@@ -77,9 +116,11 @@ export interface IPtyService {
readonly onProcessExit: Event<{ id: number, event: number | undefined }>;
readonly onProcessReady: Event<{ id: number, event: { pid: number, cwd: string } }>;
readonly onProcessTitleChanged: Event<{ id: number, event: string }>;
readonly onProcessShellTypeChanged: Event<{ id: number, event: TerminalShellType }>;
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>;
@@ -99,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>;
@@ -107,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>;
}
@@ -190,7 +240,7 @@ export interface IShellLaunchConfig {
/**
* Whether an extension is controlling the terminal via a `vscode.Pseudoterminal`.
*/
isExtensionTerminal?: boolean;
isExtensionCustomPtyTerminal?: boolean;
/**
* A UUID generated by the extension host process for terminals created on the extension host process.
@@ -200,7 +250,7 @@ export interface IShellLaunchConfig {
/**
* This is a terminal that attaches to an already running terminal.
*/
attachPersistentTerminal?: { id: number; pid: number; title: string; cwd: string; };
attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; };
/**
* Whether the terminal process environment should be exactly as provided in
@@ -226,11 +276,6 @@ export interface IShellLaunchConfig {
*/
isFeatureTerminal?: boolean;
/**
* Whether flow control is enabled for this terminal.
*/
flowControl?: boolean;
/**
* Whether this terminal was created by an extension.
*/
@@ -269,6 +314,7 @@ export interface ITerminalChildProcess {
onProcessTitleChanged: Event<string>;
onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined>;
onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig>;
onProcessShellTypeChanged: Event<TerminalShellType>;
/**
* Starts the process.
@@ -310,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...
*/
@@ -349,15 +395,15 @@ export interface ITerminalDimensions {
/**
* The columns of the terminal.
*/
readonly cols: number;
cols: number;
/**
* The rows of the terminal.
*/
readonly rows: number;
rows: number;
}
export interface ITerminalDimensionsOverride extends ITerminalDimensions {
export interface ITerminalDimensionsOverride extends Readonly<ITerminalDimensions> {
/**
* indicate that xterm must receive these exact dimensions, even if they overflow the ui!
*/

View File

@@ -31,17 +31,16 @@ export class TerminalDataBufferer implements IDisposable {
let buffer = this._terminalBufferMap.get(id);
if (buffer) {
buffer.data.push(data);
return;
}
const timeoutId = setTimeout(() => this._flushBuffer(id), throttleBy);
const timeoutId = setTimeout(() => this.flushBuffer(id), throttleBy);
buffer = {
data: [data],
timeoutId: timeoutId,
dispose: () => {
clearTimeout(timeoutId);
this._flushBuffer(id);
this.flushBuffer(id);
disposable.dispose();
}
};
@@ -57,7 +56,7 @@ export class TerminalDataBufferer implements IDisposable {
}
}
private _flushBuffer(id: number): void {
flushBuffer(id: number): void {
const buffer = this._terminalBufferMap.get(id);
if (buffer) {
this._terminalBufferMap.delete(id);

View File

@@ -38,7 +38,6 @@ export interface ICompleteTerminalConfiguration {
'terminal.integrated.inheritEnv': boolean;
'terminal.integrated.cwd': string;
'terminal.integrated.detectLocale': 'auto' | 'off' | 'on';
'terminal.flowControl': boolean;
}
export type ITerminalEnvironmentVariableCollections = [string, ISerializableEnvironmentVariableCollection][];
@@ -58,7 +57,7 @@ export interface IGetTerminalLayoutInfoArgs {
workspaceId: string;
}
export interface IPtyHostDescriptionDto {
export interface IProcessDetails {
id: number;
pid: number;
title: string;
@@ -68,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

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
import { IPtyHostProcessReplayEvent, ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';
const MAX_RECORDER_DATA_SIZE = 1024 * 1024; // 1MB
@@ -13,8 +13,6 @@ interface RecorderEntry {
data: string[];
}
export interface ReplayEntry { cols: number; rows: number; data: string; }
export interface IRemoteTerminalProcessReplayEvent {
events: ReplayEntry[];
}
@@ -22,11 +20,10 @@ export interface IRemoteTerminalProcessReplayEvent {
export class TerminalRecorder {
private _entries: RecorderEntry[];
private _totalDataLength: number;
private _totalDataLength: number = 0;
constructor(cols: number, rows: number) {
this._entries = [{ cols, rows, data: [] }];
this._totalDataLength = 0;
}
public recordResize(cols: number, rows: number): void {

View File

@@ -7,5 +7,4 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IPtyService } from 'vs/platform/terminal/common/terminal';
export const ILocalPtyService = createDecorator<ILocalPtyService>('localPtyService');
export interface ILocalPtyService extends IPtyService { }

View File

@@ -5,14 +5,14 @@
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants } from 'vs/platform/terminal/common/terminal';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalsLayoutInfo, TerminalIpcChannels, IHeartbeatService, HeartbeatConstants, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { FileAccess } from 'vs/base/common/network';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { Emitter } from 'vs/base/common/event';
import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc';
import { IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
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;
@@ -56,10 +60,14 @@ export class LocalPtyService extends Disposable implements IPtyService {
readonly onProcessReplay = this._onProcessReplay.event;
private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>());
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>());
readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>());
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
@@ -72,7 +80,7 @@ export class LocalPtyService extends Disposable implements IPtyService {
}
private _startPtyHost(): [Client, IPtyService] {
const client = this._register(new Client(
const client = new Client(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
{
serverName: 'Pty Host',
@@ -84,11 +92,13 @@ export class LocalPtyService extends Disposable implements IPtyService {
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
);
this._onPtyHostStart.fire();
// Setup heartbeat service and trigger a heartbeat immediately to reset the timeouts
const heartbeatService = ProxyChannel.toService<IHeartbeatService>(client.getChannel(TerminalIpcChannels.Heartbeat));
heartbeatService.onBeat(() => this._handleHeartbeat());
this._handleHeartbeat();
// Handle exit
this._register(client.onDidProcessExit(e => {
@@ -116,9 +126,11 @@ export class LocalPtyService extends Disposable implements IPtyService {
this._register(proxy.onProcessExit(e => this._onProcessExit.fire(e)));
this._register(proxy.onProcessReady(e => this._onProcessReady.fire(e)));
this._register(proxy.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e)));
this._register(proxy.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e)));
this._register(proxy.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e)));
this._register(proxy.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e)));
this._register(proxy.onProcessReplay(e => this._onProcessReplay.fire(e)));
this._register(proxy.onProcessOrphanQuestion(e => this._onProcessOrphanQuestion.fire(e)));
return [client, proxy];
}
@@ -141,6 +153,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);
@@ -166,7 +181,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

@@ -5,13 +5,14 @@
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById } from 'vs/platform/terminal/common/terminal';
import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess';
import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IPtyHostDescriptionDto, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IProcessDetails, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
import { ILogService } from 'vs/platform/log/common/log';
import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering';
type WorkspaceId = string;
@@ -34,10 +35,14 @@ export class PtyService extends Disposable implements IPtyService {
readonly onProcessReady = this._onProcessReady.event;
private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>());
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
private readonly _onProcessShellTypeChanged = this._register(new Emitter<{ id: number, event: TerminalShellType }>());
readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>());
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,
@@ -69,7 +74,7 @@ export class PtyService extends Disposable implements IPtyService {
workspaceId: string,
workspaceName: string
): Promise<number> {
if (shellLaunchConfig.attachPersistentTerminal) {
if (shellLaunchConfig.attachPersistentProcess) {
throw new Error('Attempt to create a process when attach object was provided');
}
const id = ++this._lastPtyId;
@@ -82,24 +87,26 @@ export class PtyService extends Disposable implements IPtyService {
if (process.onProcessResolvedShellLaunchConfig) {
process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event }));
}
const persistentTerminalProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService);
const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService);
process.onProcessExit(() => {
persistentTerminalProcess.dispose();
persistentProcess.dispose();
this._ptys.delete(id);
});
persistentTerminalProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event }));
persistentTerminalProcess.onProcessReady(event => this._onProcessReady.fire({ id, event }));
persistentTerminalProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event }));
this._ptys.set(id, persistentTerminalProcess);
persistentProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event }));
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;
}
async attachToProcess(id: number): Promise<void> {
try {
this._throwIfNoPty(id).attach();
this._logService.trace(`Persistent terminal reconnection "${id}"`);
this._logService.trace(`Persistent process reconnection "${id}"`);
} catch (e) {
this._logService.trace(`Persistent terminal reconnection "${id}" failed`, e.message);
this._logService.trace(`Persistent process reconnection "${id}" failed`, e.message);
}
}
@@ -107,6 +114,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();
}
@@ -131,6 +153,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);
@@ -150,20 +175,20 @@ 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,
activePersistentTerminalId: tab.activePersistentTerminalId,
activePersistentProcessId: tab.activePersistentProcessId,
terminals: filtered
};
}
private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise<IRawTerminalInstanceLayoutInfo<IPtyHostDescriptionDto | null>> {
private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise<IRawTerminalInstanceLayoutInfo<IProcessDetails | null>> {
try {
const persistentTerminalProcess = this._throwIfNoPty(t.terminal);
const termDto = persistentTerminalProcess && await this._terminalToDto(t.terminal, persistentTerminalProcess);
const persistentProcess = this._throwIfNoPty(t.terminal);
const processDetails = persistentProcess && await this._buildProcessDetails(t.terminal, persistentProcess);
return {
terminal: termDto ?? null,
terminal: processDetails ?? null,
relativeSize: t.relativeSize
};
} catch (e) {
@@ -176,14 +201,14 @@ export class PtyService extends Disposable implements IPtyService {
}
}
private async _terminalToDto(id: number, persistentTerminalProcess: PersistentTerminalProcess): Promise<IPtyHostDescriptionDto> {
const [cwd, isOrphan] = await Promise.all([persistentTerminalProcess.getCwd(), persistentTerminalProcess.isOrphaned()]);
private async _buildProcessDetails(id: number, persistentProcess: PersistentTerminalProcess): Promise<IProcessDetails> {
const [cwd, isOrphan] = await Promise.all([persistentProcess.getCwd(), persistentProcess.isOrphaned()]);
return {
id,
title: persistentTerminalProcess.title,
pid: persistentTerminalProcess.pid,
workspaceId: persistentTerminalProcess.workspaceId,
workspaceName: persistentTerminalProcess.workspaceName,
title: persistentProcess.title,
pid: persistentProcess.pid,
workspaceId: persistentProcess.workspaceId,
workspaceName: persistentProcess.workspaceName,
cwd,
isOrphan
};
@@ -200,7 +225,7 @@ export class PtyService extends Disposable implements IPtyService {
export class PersistentTerminalProcess extends Disposable {
// private readonly _bufferer: TerminalDataBufferer;
private readonly _bufferer: TerminalDataBufferer;
private readonly _pendingCommands = new Map<number, { resolve: (data: any) => void; reject: (err: any) => void; }>();
@@ -219,10 +244,14 @@ export class PersistentTerminalProcess extends Disposable {
readonly onProcessReady = this._onProcessReady.event;
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>());
readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensionsOverride | undefined>());
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;
@@ -233,7 +262,7 @@ export class PersistentTerminalProcess extends Disposable {
get title(): string { return this._terminalProcess.currentTitle; }
constructor(
private _persistentTerminalId: number,
private _persistentProcessId: number,
private readonly _terminalProcess: TerminalProcess,
public readonly workspaceId: string,
public readonly workspaceName: string,
@@ -246,36 +275,29 @@ export class PersistentTerminalProcess extends Disposable {
this._orphanQuestionBarrier = null;
this._orphanQuestionReplyTime = 0;
this._disconnectRunner1 = this._register(new RunOnceScheduler(() => {
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`);
this._logService.info(`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, shutting down pid "${this._pid}"`);
this.shutdown(true);
}, LocalReconnectConstants.ReconnectionGraceTime));
this._disconnectRunner2 = this._register(new RunOnceScheduler(() => {
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`);
this._logService.info(`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, shutting down pid ${this._pid}`);
this.shutdown(true);
}, LocalReconnectConstants.ReconnectionShortGraceTime));
// TODO: Bring back bufferer
// this._bufferer = new TerminalDataBufferer((id, data) => {
// const ev: IPtyHostProcessDataEvent = {
// type: 'data',
// data: data
// };
// this._events.fire(ev);
// });
this._register(this._terminalProcess.onProcessReady(e => {
this._pid = e.pid;
this._cwd = e.cwd;
this._onProcessReady.fire(e);
}));
this._register(this._terminalProcess.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e)));
this._register(this._terminalProcess.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e)));
// Buffer data events to reduce the amount of messages going to the renderer
// this._register(this._bufferer.startBuffering(this._persistentTerminalId, this._terminalProcess.onProcessData));
this._register(this._terminalProcess.onProcessData(e => this._recorder.recordData(e)));
this._register(this._terminalProcess.onProcessExit(exitCode => {
// this._bufferer.stopBuffering(this._persistentTerminalId);
}));
// Data buffering to reduce the amount of messages going to the renderer
this._bufferer = new TerminalDataBufferer((_, data) => this._onProcessData.fire({ data: data, sync: true }));
this._register(this._bufferer.startBuffering(this._persistentProcessId, this._terminalProcess.onProcessData));
this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId)));
// Data recording for reconnect
this._register(this.onProcessData(e => this._recorder.recordData(e.data)));
}
attach(): void {
@@ -301,6 +323,7 @@ export class PersistentTerminalProcess extends Disposable {
} else {
this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd });
this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle);
this._onProcessShellTypeChanged.fire(this._terminalProcess.shellType);
this.triggerReplay();
}
return undefined;
@@ -319,6 +342,9 @@ export class PersistentTerminalProcess extends Disposable {
return;
}
this._recorder.recordResize(cols, rows);
// Buffered events should flush when a resize occurs
this._bufferer.flushBuffer(this._persistentProcessId);
return this._terminalProcess.resize(cols, rows);
}
acknowledgeDataEvent(charCount: number): void {
@@ -344,7 +370,7 @@ export class PersistentTerminalProcess extends Disposable {
dataLength += e.data.length;
}
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": Replaying ${dataLength} chars and ${ev.events.length} size events`);
this._logService.info(`Persistent process "${this._persistentProcessId}": Replaying ${dataLength} chars and ${ev.events.length} size events`);
this._onProcessReplay.fire(ev);
this._terminalProcess.clearUnacknowledgedChars();
}
@@ -357,7 +383,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;
@@ -382,19 +408,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

@@ -5,6 +5,7 @@
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as process from 'vs/base/common/process';
import { exists } from 'vs/base/node/pfs';
import { isString } from 'vs/base/common/types';
import { getCaseInsensitive } from 'vs/base/common/objects';

View File

@@ -10,12 +10,13 @@ import * as fs from 'fs';
import * as os from 'os';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride } from 'vs/platform/terminal/common/terminal';
import { IShellLaunchConfig, ITerminalLaunchError, FlowControlConstants, ITerminalChildProcess, ITerminalDimensionsOverride, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { exec } from 'child_process';
import { ILogService } from 'vs/platform/log/common/log';
import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper';
// Writing large amounts of data can be corrupted for some reason, after looking into this is
// appears to be a race condition around writing to the FD which may be based on how powerful the
@@ -26,7 +27,7 @@ const WRITE_INTERVAL_MS = 5;
const enum ShutdownConstants {
/**
* The amount of time that must pass between data events after exit is queued before the actual
* The amount of ms that must pass between data events after exit is queued before the actual
* kill call is triggered. This data flush mechanism works around an [issue in node-pty][1]
* where not all data is flushed which causes problems for task problem matchers. Additionally
* on Windows under conpty, killing a process while data is being output will cause the [conhost
@@ -38,7 +39,7 @@ const enum ShutdownConstants {
*/
DataFlushTimeout = 250,
/**
* The maximum time to allow after dispose is called because forcefully killing the process.
* The maximum ms to allow after dispose is called because forcefully killing the process.
*/
MaximumShutdownTime = 5000
}
@@ -54,6 +55,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
private _currentTitle: string = '';
private _processStartupComplete: Promise<void> | undefined;
private _isDisposed: boolean = false;
private _windowsShellHelper: WindowsShellHelper | undefined;
private _titleInterval: NodeJS.Timer | null = null;
private _writeQueue: string[] = [];
private _writeTimeout: NodeJS.Timeout | undefined;
@@ -63,9 +65,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
private _isPtyPaused: boolean = false;
private _unacknowledgedCharCount: number = 0;
public get exitMessage(): string | undefined { return this._exitMessage; }
public get currentTitle(): string { return this._currentTitle; }
public get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; }
public get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; }
private readonly _onProcessData = this._register(new Emitter<string>());
public get onProcessData(): Event<string> { return this._onProcessData.event; }
@@ -75,6 +78,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; }
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
private readonly _onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>());
public readonly onProcessShellTypeChanged = this._onProcessShellTypeChanged.event;
constructor(
private readonly _shellLaunchConfig: IShellLaunchConfig,
@@ -111,15 +116,23 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
conptyInheritCursor: useConpty && !!_shellLaunchConfig.initialText
};
// Delay resizes to avoid conpty not respecting very early resize calls
if (platform.isWindows && useConpty && cols === 0 && rows === 0 && this._shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) {
this._delayedResizer = new DelayedResizer();
this._register(this._delayedResizer.onTrigger(dimensions => {
this._delayedResizer?.dispose();
this._delayedResizer = undefined;
if (dimensions.cols && dimensions.rows) {
this.resize(dimensions.cols, dimensions.rows);
}
}));
if (platform.isWindows) {
if (useConpty && cols === 0 && rows === 0 && this._shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) {
this._delayedResizer = new DelayedResizer();
this._register(this._delayedResizer.onTrigger(dimensions => {
this._delayedResizer?.dispose();
this._delayedResizer = undefined;
if (dimensions.cols && dimensions.rows) {
this.resize(dimensions.cols, dimensions.rows);
}
}));
}
// WindowsShellHelper is used to fetch the process title and shell type
this.onProcessReady(e => {
this._windowsShellHelper = this._register(new WindowsShellHelper(e.pid));
this._register(this._windowsShellHelper.onShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e)));
this._register(this._windowsShellHelper.onShellNameChanged(e => this._onProcessTitleChanged.fire(e)));
});
}
}
onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined> | undefined;
@@ -188,19 +201,22 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
this.onProcessReady(() => c());
});
ptyProcess.onData(data => {
if (this._shellLaunchConfig.flowControl) {
this._unacknowledgedCharCount += data.length;
if (!this._isPtyPaused && this._unacknowledgedCharCount > FlowControlConstants.HighWatermarkChars) {
this._logService.trace(`Flow control: Pause (${this._unacknowledgedCharCount} > ${FlowControlConstants.HighWatermarkChars})`);
this._isPtyPaused = true;
ptyProcess.pause();
}
// Handle flow control
this._unacknowledgedCharCount += data.length;
if (!this._isPtyPaused && this._unacknowledgedCharCount > FlowControlConstants.HighWatermarkChars) {
this._logService.trace(`Flow control: Pause (${this._unacknowledgedCharCount} > ${FlowControlConstants.HighWatermarkChars})`);
this._isPtyPaused = true;
ptyProcess.pause();
}
// Refire the data event
this._onProcessData.fire(data);
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
this._queueProcessExit();
}
this._windowsShellHelper?.checkShell();
});
ptyProcess.onExit(e => {
this._exitCode = e.exitCode;
@@ -216,10 +232,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
clearInterval(this._titleInterval);
}
this._titleInterval = null;
this._onProcessData.dispose();
this._onProcessExit.dispose();
this._onProcessReady.dispose();
this._onProcessTitleChanged.dispose();
super.dispose();
}
@@ -369,9 +381,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
}
public acknowledgeDataEvent(charCount: number): void {
if (!this._shellLaunchConfig.flowControl) {
return;
}
// Prevent lower than 0 to heal from errors
this._unacknowledgedCharCount = Math.max(this._unacknowledgedCharCount - charCount, 0);
this._logService.trace(`Flow control: Ack ${charCount} chars (unacknowledged: ${this._unacknowledgedCharCount})`);
@@ -383,10 +392,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
}
public clearUnacknowledgedChars(): void {
if (!this._shellLaunchConfig.flowControl) {
return;
}
this._unacknowledgedCharCount = 0;
this._logService.trace(`Flow control: Cleared all unacknowledged chars, forcing resume`);
if (this._isPtyPaused) {

View File

@@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as platform from 'vs/base/common/platform';
import { Emitter, Event } from 'vs/base/common/event';
import type * as WindowsProcessTreeType from 'windows-process-tree';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { debounce } from 'vs/base/common/decorators';
import { timeout } from 'vs/base/common/async';
export interface IWindowsShellHelper extends IDisposable {
readonly onShellNameChanged: Event<string>;
readonly onShellTypeChanged: Event<TerminalShellType>;
getShellType(title: string): TerminalShellType;
getShellName(): Promise<string>;
}
const SHELL_EXECUTABLES = [
'cmd.exe',
'powershell.exe',
'pwsh.exe',
'bash.exe',
'wsl.exe',
'ubuntu.exe',
'ubuntu1804.exe',
'kali.exe',
'debian.exe',
'opensuse-42.exe',
'sles-12.exe'
];
let windowsProcessTree: typeof WindowsProcessTreeType;
export class WindowsShellHelper extends Disposable implements IWindowsShellHelper {
private _isDisposed: boolean;
private _currentRequest: Promise<string> | undefined;
private _shellType: TerminalShellType | undefined;
public get shellType(): TerminalShellType | undefined { return this._shellType; }
private _shellTitle: string = '';
public get shellTitle(): string { return this._shellTitle; }
private readonly _onShellNameChanged = new Emitter<string>();
public get onShellNameChanged(): Event<string> { return this._onShellNameChanged.event; }
private readonly _onShellTypeChanged = new Emitter<TerminalShellType>();
public get onShellTypeChanged(): Event<TerminalShellType> { return this._onShellTypeChanged.event; }
public constructor(
private _rootProcessId: number
) {
super();
if (!platform.isWindows) {
throw new Error(`WindowsShellHelper cannot be instantiated on ${platform.platform}`);
}
this._isDisposed = false;
this._startMonitoringShell();
}
private async _startMonitoringShell(): Promise<void> {
if (this._isDisposed) {
return;
}
this.checkShell();
}
@debounce(500)
async checkShell(): Promise<void> {
if (platform.isWindows) {
// Wait to give the shell some time to actually launch a process, this
// could lead to a race condition but it would be recovered from when
// data stops and should cover the majority of cases
await timeout(300);
this.getShellName().then(title => {
const type = this.getShellType(title);
if (type !== this._shellType) {
this._onShellTypeChanged.fire(type);
this._onShellNameChanged.fire(title);
this._shellType = type;
this._shellTitle = title;
}
});
}
}
private traverseTree(tree: any): string {
if (!tree) {
return '';
}
if (SHELL_EXECUTABLES.indexOf(tree.name) === -1) {
return tree.name;
}
if (!tree.children || tree.children.length === 0) {
return tree.name;
}
let favouriteChild = 0;
for (; favouriteChild < tree.children.length; favouriteChild++) {
const child = tree.children[favouriteChild];
if (!child.children || child.children.length === 0) {
break;
}
if (child.children[0].name !== 'conhost.exe') {
break;
}
}
if (favouriteChild >= tree.children.length) {
return tree.name;
}
return this.traverseTree(tree.children[favouriteChild]);
}
public dispose(): void {
this._isDisposed = true;
super.dispose();
}
/**
* Returns the innermost shell executable running in the terminal
*/
public getShellName(): Promise<string> {
if (this._isDisposed) {
return Promise.resolve('');
}
// Prevent multiple requests at once, instead return current request
if (this._currentRequest) {
return this._currentRequest;
}
this._currentRequest = new Promise<string>(async resolve => {
if (!windowsProcessTree) {
windowsProcessTree = await import('windows-process-tree');
}
windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => {
const name = this.traverseTree(tree);
this._currentRequest = undefined;
resolve(name);
});
});
return this._currentRequest;
}
public getShellType(executable: string): TerminalShellType {
switch (executable.toLowerCase()) {
case 'cmd.exe':
return WindowsShellType.CommandPrompt;
case 'powershell.exe':
case 'pwsh.exe':
return WindowsShellType.PowerShell;
case 'bash.exe':
case 'git-cmd.exe':
return WindowsShellType.GitBash;
case 'wsl.exe':
case 'ubuntu.exe':
case 'ubuntu1804.exe':
case 'kali.exe':
case 'debian.exe':
case 'opensuse-42.exe':
case 'sles-12.exe':
return WindowsShellType.Wsl;
default:
return undefined;
}
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
import { ReplayEntry } from 'vs/platform/terminal/common/terminalProcess';
function eventsEqual(recorder: TerminalRecorder, expected: ReplayEntry[]) {
const actual = recorder.generateReplayEvent().events;
for (let i = 0; i < expected.length; i++) {
assert.deepStrictEqual(actual[i], expected[i]);
}
}
suite('TerminalRecorder', () => {
test('should record dimensions', () => {
const recorder = new TerminalRecorder(1, 2);
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: '' }
]);
recorder.recordData('a');
recorder.recordResize(3, 4);
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: 'a' },
{ cols: 3, rows: 4, data: '' }
]);
});
test('should ignore resize events without data', () => {
const recorder = new TerminalRecorder(1, 2);
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: '' }
]);
recorder.recordResize(3, 4);
eventsEqual(recorder, [
{ cols: 3, rows: 4, data: '' }
]);
});
test('should record data and combine it into the previous resize event', () => {
const recorder = new TerminalRecorder(1, 2);
recorder.recordData('a');
recorder.recordData('b');
recorder.recordResize(3, 4);
recorder.recordData('c');
recorder.recordData('d');
eventsEqual(recorder, [
{ cols: 1, rows: 2, data: 'ab' },
{ cols: 3, rows: 4, data: 'cd' }
]);
});
});