mirror of
https://github.com/coder/code-server.git
synced 2026-06-16 06:57:11 +02:00
chore(vscode): update to 1.55.2
This commit is contained in:
@@ -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!
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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> {
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
166
lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts
Normal file
166
lib/vscode/src/vs/platform/terminal/node/windowsShellHelper.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user