mirror of
https://github.com/coder/code-server.git
synced 2026-06-21 17:37:11 +02:00
chore(vscode): update to 1.54.2
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export enum EnvironmentVariableMutatorType {
|
||||
Replace = 1,
|
||||
Append = 2,
|
||||
Prepend = 3
|
||||
}
|
||||
export interface IEnvironmentVariableMutator {
|
||||
readonly value: string;
|
||||
readonly type: EnvironmentVariableMutatorType;
|
||||
}
|
||||
/** [variable, mutator] */
|
||||
export type ISerializableEnvironmentVariableCollection = [string, IEnvironmentVariableMutator][];
|
||||
365
lib/vscode/src/vs/platform/terminal/common/terminal.ts
Normal file
365
lib/vscode/src/vs/platform/terminal/common/terminal.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
|
||||
|
||||
export interface IRawTerminalInstanceLayoutInfo<T> {
|
||||
relativeSize: number;
|
||||
terminal: T;
|
||||
}
|
||||
export type ITerminalInstanceLayoutInfoById = IRawTerminalInstanceLayoutInfo<number>;
|
||||
export type ITerminalInstanceLayoutInfo = IRawTerminalInstanceLayoutInfo<IPtyHostAttachTarget>;
|
||||
|
||||
export interface IRawTerminalTabLayoutInfo<T> {
|
||||
isActive: boolean;
|
||||
activePersistentTerminalId: number | undefined;
|
||||
terminals: IRawTerminalInstanceLayoutInfo<T>[];
|
||||
}
|
||||
|
||||
export type ITerminalTabLayoutInfoById = IRawTerminalTabLayoutInfo<number>;
|
||||
export type ITerminalTabLayoutInfo = IRawTerminalTabLayoutInfo<IPtyHostAttachTarget | null>;
|
||||
|
||||
export interface IRawTerminalsLayoutInfo<T> {
|
||||
tabs: IRawTerminalTabLayoutInfo<T>[];
|
||||
}
|
||||
|
||||
export interface IPtyHostAttachTarget {
|
||||
id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
cwd: string;
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
isOrphan: boolean;
|
||||
}
|
||||
|
||||
export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo<IPtyHostAttachTarget | null>;
|
||||
export type ITerminalsLayoutInfoById = IRawTerminalsLayoutInfo<number>;
|
||||
|
||||
export interface IRawTerminalInstanceLayoutInfo<T> {
|
||||
relativeSize: number;
|
||||
terminal: T;
|
||||
}
|
||||
|
||||
export enum TerminalIpcChannels {
|
||||
/**
|
||||
* Communicates between the renderer process and shared process.
|
||||
*/
|
||||
LocalPty = 'localPty',
|
||||
/**
|
||||
* Communicates between the shared process and the pty host process.
|
||||
*/
|
||||
PtyHost = 'ptyHost',
|
||||
/**
|
||||
* Deals with logging from the pty host process.
|
||||
*/
|
||||
Log = 'log',
|
||||
/**
|
||||
* Enables the detection of unresponsive pty hosts.
|
||||
*/
|
||||
Heartbeat = 'heartbeat'
|
||||
}
|
||||
|
||||
export interface IPtyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onPtyHostExit?: Event<number>;
|
||||
readonly onPtyHostStart?: Event<void>;
|
||||
readonly onPtyHostUnresponsive?: Event<void>;
|
||||
readonly onPtyHostResponsive?: Event<void>;
|
||||
|
||||
readonly onProcessData: Event<{ id: number, event: IProcessDataEvent | string }>;
|
||||
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 onProcessOverrideDimensions: Event<{ id: number, event: ITerminalDimensionsOverride | undefined }>;
|
||||
readonly onProcessResolvedShellLaunchConfig: Event<{ id: number, event: IShellLaunchConfig }>;
|
||||
readonly onProcessReplay: Event<{ id: number, event: IPtyHostProcessReplayEvent }>;
|
||||
|
||||
restartPtyHost?(): Promise<void>;
|
||||
shutdownAll?(): Promise<void>;
|
||||
|
||||
createProcess(
|
||||
shellLaunchConfig: IShellLaunchConfig,
|
||||
cwd: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
env: IProcessEnvironment,
|
||||
executableEnv: IProcessEnvironment,
|
||||
windowsEnableConpty: boolean,
|
||||
shouldPersist: boolean,
|
||||
workspaceId: string,
|
||||
workspaceName: string
|
||||
): Promise<number>;
|
||||
attachToProcess(id: number): Promise<void>;
|
||||
detachFromProcess(id: number): Promise<void>;
|
||||
|
||||
start(id: number): Promise<ITerminalLaunchError | undefined>;
|
||||
shutdown(id: number, immediate: boolean): Promise<void>;
|
||||
input(id: number, data: string): Promise<void>;
|
||||
resize(id: number, cols: number, rows: number): Promise<void>;
|
||||
getInitialCwd(id: number): Promise<string>;
|
||||
getCwd(id: number): Promise<string>;
|
||||
getLatency(id: number): Promise<number>;
|
||||
acknowledgeDataEvent(id: number, charCount: number): Promise<void>;
|
||||
|
||||
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void;
|
||||
getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined>;
|
||||
}
|
||||
|
||||
export enum HeartbeatConstants {
|
||||
/**
|
||||
* The duration between heartbeats
|
||||
*/
|
||||
BeatInterval = 5000,
|
||||
/**
|
||||
* Defines a multiplier for BeatInterval for how long to wait before starting the second wait
|
||||
* timer.
|
||||
*/
|
||||
FirstWaitMultiplier = 1.2,
|
||||
/**
|
||||
* Defines a multiplier for BeatInterval for how long to wait before telling the user about
|
||||
* non-responsiveness. The second timer is to avoid informing the user incorrectly when waking
|
||||
* the computer up from sleep
|
||||
*/
|
||||
SecondWaitMultiplier = 1,
|
||||
/**
|
||||
* How long to wait before telling the user about non-responsiveness when they try to create a
|
||||
* process. This short circuits the standard wait timeouts to tell the user sooner and only
|
||||
* create process is handled to avoid additional perf overhead.
|
||||
*/
|
||||
CreateProcessTimeout = 5000
|
||||
}
|
||||
|
||||
export interface IHeartbeatService {
|
||||
readonly onBeat: Event<void>;
|
||||
}
|
||||
|
||||
export interface IShellLaunchConfig {
|
||||
/**
|
||||
* The name of the terminal, if this is not set the name of the process will be used.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* The shell executable (bash, cmd, etc.).
|
||||
*/
|
||||
executable?: string;
|
||||
|
||||
/**
|
||||
* The CLI arguments to use with executable, a string[] is in argv format and will be escaped,
|
||||
* a string is in "CommandLine" pre-escaped format and will be used as is. The string option is
|
||||
* only supported on Windows and will throw an exception if used on macOS or Linux.
|
||||
*/
|
||||
args?: string[] | string;
|
||||
|
||||
/**
|
||||
* The current working directory of the terminal, this overrides the `terminal.integrated.cwd`
|
||||
* settings key.
|
||||
*/
|
||||
cwd?: string | URI;
|
||||
|
||||
/**
|
||||
* A custom environment for the terminal, if this is not set the environment will be inherited
|
||||
* from the VS Code process.
|
||||
*/
|
||||
env?: ITerminalEnvironment;
|
||||
|
||||
/**
|
||||
* Whether to ignore a custom cwd from the `terminal.integrated.cwd` settings key (e.g. if the
|
||||
* shell is being launched by an extension).
|
||||
*/
|
||||
ignoreConfigurationCwd?: boolean;
|
||||
|
||||
/** Whether to wait for a key press before closing the terminal. */
|
||||
waitOnExit?: boolean | string;
|
||||
|
||||
/**
|
||||
* A string including ANSI escape sequences that will be written to the terminal emulator
|
||||
* _before_ the terminal process has launched, a trailing \n is added at the end of the string.
|
||||
* This allows for example the terminal instance to display a styled message as the first line
|
||||
* of the terminal. Use \x1b over \033 or \e for the escape control character.
|
||||
*/
|
||||
initialText?: string;
|
||||
|
||||
/**
|
||||
* Whether an extension is controlling the terminal via a `vscode.Pseudoterminal`.
|
||||
*/
|
||||
isExtensionTerminal?: boolean;
|
||||
|
||||
/**
|
||||
* A UUID generated by the extension host process for terminals created on the extension host process.
|
||||
*/
|
||||
extHostTerminalId?: string;
|
||||
|
||||
/**
|
||||
* This is a terminal that attaches to an already running terminal.
|
||||
*/
|
||||
attachPersistentTerminal?: { id: number; pid: number; title: string; cwd: string; };
|
||||
|
||||
/**
|
||||
* Whether the terminal process environment should be exactly as provided in
|
||||
* `TerminalOptions.env`. When this is false (default), the environment will be based on the
|
||||
* window's environment and also apply configured platform settings like
|
||||
* `terminal.integrated.windows.env` on top. When this is true, the complete environment must be
|
||||
* provided as nothing will be inherited from the process or any configuration.
|
||||
*/
|
||||
strictEnv?: boolean;
|
||||
|
||||
/**
|
||||
* When enabled the terminal will run the process as normal but not be surfaced to the user
|
||||
* until `Terminal.show` is called. The typical usage for this is when you need to run
|
||||
* something that may need interactivity but only want to tell the user about it when
|
||||
* interaction is needed. Note that the terminals will still be exposed to all extensions
|
||||
* as normal.
|
||||
*/
|
||||
hideFromUser?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this terminal is not a terminal that the user directly created and uses, but rather
|
||||
* a terminal used to drive some VS Code feature.
|
||||
*/
|
||||
isFeatureTerminal?: boolean;
|
||||
|
||||
/**
|
||||
* Whether flow control is enabled for this terminal.
|
||||
*/
|
||||
flowControl?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this terminal was created by an extension.
|
||||
*/
|
||||
isExtensionOwnedTerminal?: boolean;
|
||||
}
|
||||
|
||||
export interface ITerminalEnvironment {
|
||||
[key: string]: string | null;
|
||||
}
|
||||
|
||||
export interface ITerminalLaunchError {
|
||||
message: string;
|
||||
code?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface representing a raw terminal child process, this contains a subset of the
|
||||
* child_process.ChildProcess node.js interface.
|
||||
*/
|
||||
export interface ITerminalChildProcess {
|
||||
/**
|
||||
* A unique identifier for the terminal process. Note that the uniqueness only applies to a
|
||||
* given pty service connection, IDs will be duplicated for remote and local terminals for
|
||||
* example. The ID will be 0 if it does not support reconnection.
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* Whether the process should be persisted across reloads.
|
||||
*/
|
||||
shouldPersist: boolean;
|
||||
|
||||
onProcessData: Event<IProcessDataEvent | string>;
|
||||
onProcessExit: Event<number | undefined>;
|
||||
onProcessReady: Event<{ pid: number, cwd: string }>;
|
||||
onProcessTitleChanged: Event<string>;
|
||||
onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined>;
|
||||
onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig>;
|
||||
|
||||
/**
|
||||
* Starts the process.
|
||||
*
|
||||
* @returns undefined when the process was successfully started, otherwise an object containing
|
||||
* information on what went wrong.
|
||||
*/
|
||||
start(): Promise<ITerminalLaunchError | undefined>;
|
||||
|
||||
/**
|
||||
* Detach the process from the UI and await reconnect.
|
||||
*/
|
||||
detach?(): void;
|
||||
|
||||
/**
|
||||
* Shutdown the terminal process.
|
||||
*
|
||||
* @param immediate When true the process will be killed immediately, otherwise the process will
|
||||
* be given some time to make sure no additional data comes through.
|
||||
*/
|
||||
shutdown(immediate: boolean): void;
|
||||
input(data: string): void;
|
||||
resize(cols: number, rows: number): void;
|
||||
|
||||
/**
|
||||
* Acknowledge a data event has been parsed by the terminal, this is used to implement flow
|
||||
* control to ensure remote processes to not get too far ahead of the client and flood the
|
||||
* connection.
|
||||
* @param charCount The number of characters being acknowledged.
|
||||
*/
|
||||
acknowledgeDataEvent(charCount: number): void;
|
||||
|
||||
getInitialCwd(): Promise<string>;
|
||||
getCwd(): Promise<string>;
|
||||
getLatency(): Promise<number>;
|
||||
}
|
||||
|
||||
export const enum LocalReconnectConstants {
|
||||
/**
|
||||
* If there is no reconnection within this time-frame, consider the connection permanently closed...
|
||||
*/
|
||||
ReconnectionGraceTime = 30000, // 30 seconds
|
||||
/**
|
||||
* Maximal grace time between the first and the last reconnection...
|
||||
*/
|
||||
ReconnectionShortGraceTime = 6000, // 6 seconds
|
||||
}
|
||||
|
||||
export const enum FlowControlConstants {
|
||||
/**
|
||||
* The number of _unacknowledged_ chars to have been sent before the pty is paused in order for
|
||||
* the client to catch up.
|
||||
*/
|
||||
HighWatermarkChars = 100000,
|
||||
/**
|
||||
* After flow control pauses the pty for the client the catch up, this is the number of
|
||||
* _unacknowledged_ chars to have been caught up to on the client before resuming the pty again.
|
||||
* This is used to attempt to prevent pauses in the flowing data; ideally while the pty is
|
||||
* paused the number of unacknowledged chars would always be greater than 0 or the client will
|
||||
* appear to stutter. In reality this balance is hard to accomplish though so heavy commands
|
||||
* will likely pause as latency grows, not flooding the connection is the important thing as
|
||||
* it's shared with other core functionality.
|
||||
*/
|
||||
LowWatermarkChars = 5000,
|
||||
/**
|
||||
* The number characters that are accumulated on the client side before sending an ack event.
|
||||
* This must be less than or equal to LowWatermarkChars or the terminal max never unpause.
|
||||
*/
|
||||
CharCountAckSize = 5000
|
||||
}
|
||||
|
||||
export interface IProcessDataEvent {
|
||||
data: string;
|
||||
sync: boolean;
|
||||
}
|
||||
|
||||
export interface ITerminalDimensions {
|
||||
/**
|
||||
* The columns of the terminal.
|
||||
*/
|
||||
readonly cols: number;
|
||||
|
||||
/**
|
||||
* The rows of the terminal.
|
||||
*/
|
||||
readonly rows: number;
|
||||
}
|
||||
|
||||
export interface ITerminalDimensionsOverride extends ITerminalDimensions {
|
||||
/**
|
||||
* indicate that xterm must receive these exact dimensions, even if they overflow the ui!
|
||||
*/
|
||||
forceExactSize?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IProcessDataEvent } from 'vs/platform/terminal/common/terminal';
|
||||
|
||||
interface TerminalDataBuffer extends IDisposable {
|
||||
data: string[];
|
||||
timeoutId: any;
|
||||
}
|
||||
|
||||
export class TerminalDataBufferer implements IDisposable {
|
||||
private readonly _terminalBufferMap = new Map<number, TerminalDataBuffer>();
|
||||
|
||||
constructor(private readonly _callback: (id: number, data: string) => void) {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
for (const buffer of this._terminalBufferMap.values()) {
|
||||
buffer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
startBuffering(id: number, event: Event<string | IProcessDataEvent>, throttleBy: number = 5): IDisposable {
|
||||
let disposable: IDisposable;
|
||||
disposable = event((e: string | IProcessDataEvent) => {
|
||||
const data = (typeof e === 'string' ? e : e.data);
|
||||
let buffer = this._terminalBufferMap.get(id);
|
||||
if (buffer) {
|
||||
buffer.data.push(data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => this._flushBuffer(id), throttleBy);
|
||||
buffer = {
|
||||
data: [data],
|
||||
timeoutId: timeoutId,
|
||||
dispose: () => {
|
||||
clearTimeout(timeoutId);
|
||||
this._flushBuffer(id);
|
||||
disposable.dispose();
|
||||
}
|
||||
};
|
||||
this._terminalBufferMap.set(id, buffer);
|
||||
});
|
||||
return disposable;
|
||||
}
|
||||
|
||||
stopBuffering(id: number) {
|
||||
const buffer = this._terminalBufferMap.get(id);
|
||||
if (buffer) {
|
||||
buffer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private _flushBuffer(id: number): void {
|
||||
const buffer = this._terminalBufferMap.get(id);
|
||||
if (buffer) {
|
||||
this._terminalBufferMap.delete(id);
|
||||
this._callback(id, buffer.data.join(''));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { UriComponents } from 'vs/base/common/uri';
|
||||
import { IRawTerminalTabLayoutInfo, ITerminalEnvironment, ITerminalTabLayoutInfoById } from 'vs/platform/terminal/common/terminal';
|
||||
import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable';
|
||||
|
||||
export interface IShellLaunchConfigDto {
|
||||
name?: string;
|
||||
executable?: string;
|
||||
args?: string[] | string;
|
||||
cwd?: string | UriComponents;
|
||||
env?: { [key: string]: string | null; };
|
||||
hideFromUser?: boolean;
|
||||
}
|
||||
|
||||
export interface ISingleTerminalConfiguration<T> {
|
||||
userValue: T | undefined;
|
||||
value: T | undefined;
|
||||
defaultValue: T | undefined;
|
||||
}
|
||||
|
||||
export interface ICompleteTerminalConfiguration {
|
||||
'terminal.integrated.automationShell.windows': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.automationShell.osx': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.automationShell.linux': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.shell.windows': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.shell.osx': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.shell.linux': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.shellArgs.windows': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.shellArgs.osx': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.shellArgs.linux': ISingleTerminalConfiguration<string | string[]>;
|
||||
'terminal.integrated.env.windows': ISingleTerminalConfiguration<ITerminalEnvironment>;
|
||||
'terminal.integrated.env.osx': ISingleTerminalConfiguration<ITerminalEnvironment>;
|
||||
'terminal.integrated.env.linux': ISingleTerminalConfiguration<ITerminalEnvironment>;
|
||||
'terminal.integrated.inheritEnv': boolean;
|
||||
'terminal.integrated.cwd': string;
|
||||
'terminal.integrated.detectLocale': 'auto' | 'off' | 'on';
|
||||
'terminal.flowControl': boolean;
|
||||
}
|
||||
|
||||
export type ITerminalEnvironmentVariableCollections = [string, ISerializableEnvironmentVariableCollection][];
|
||||
|
||||
export interface IWorkspaceFolderData {
|
||||
uri: UriComponents;
|
||||
name: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface ISetTerminalLayoutInfoArgs {
|
||||
workspaceId: string;
|
||||
tabs: ITerminalTabLayoutInfoById[];
|
||||
}
|
||||
|
||||
export interface IGetTerminalLayoutInfoArgs {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface IPtyHostDescriptionDto {
|
||||
id: number;
|
||||
pid: number;
|
||||
title: string;
|
||||
cwd: string;
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
isOrphan: boolean;
|
||||
}
|
||||
|
||||
export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IPtyHostDescriptionDto>;
|
||||
|
||||
export interface ReplayEntry { cols: number; rows: number; data: string; }
|
||||
export interface IPtyHostProcessReplayEvent {
|
||||
events: ReplayEntry[];
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
|
||||
|
||||
const MAX_RECORDER_DATA_SIZE = 1024 * 1024; // 1MB
|
||||
|
||||
interface RecorderEntry {
|
||||
cols: number;
|
||||
rows: number;
|
||||
data: string[];
|
||||
}
|
||||
|
||||
export interface ReplayEntry { cols: number; rows: number; data: string; }
|
||||
|
||||
export interface IRemoteTerminalProcessReplayEvent {
|
||||
events: ReplayEntry[];
|
||||
}
|
||||
|
||||
export class TerminalRecorder {
|
||||
|
||||
private _entries: RecorderEntry[];
|
||||
private _totalDataLength: number;
|
||||
|
||||
constructor(cols: number, rows: number) {
|
||||
this._entries = [{ cols, rows, data: [] }];
|
||||
this._totalDataLength = 0;
|
||||
}
|
||||
|
||||
public recordResize(cols: number, rows: number): void {
|
||||
if (this._entries.length > 0) {
|
||||
const lastEntry = this._entries[this._entries.length - 1];
|
||||
if (lastEntry.data.length === 0) {
|
||||
// last entry is just a resize, so just remove it
|
||||
this._entries.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._entries.length > 0) {
|
||||
const lastEntry = this._entries[this._entries.length - 1];
|
||||
if (lastEntry.cols === cols && lastEntry.rows === rows) {
|
||||
// nothing changed
|
||||
return;
|
||||
}
|
||||
if (lastEntry.cols === 0 && lastEntry.rows === 0) {
|
||||
// we finally received a good size!
|
||||
lastEntry.cols = cols;
|
||||
lastEntry.rows = rows;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._entries.push({ cols, rows, data: [] });
|
||||
}
|
||||
|
||||
public recordData(data: string): void {
|
||||
const lastEntry = this._entries[this._entries.length - 1];
|
||||
lastEntry.data.push(data);
|
||||
|
||||
this._totalDataLength += data.length;
|
||||
while (this._totalDataLength > MAX_RECORDER_DATA_SIZE) {
|
||||
const firstEntry = this._entries[0];
|
||||
const remainingToDelete = this._totalDataLength - MAX_RECORDER_DATA_SIZE;
|
||||
if (remainingToDelete >= firstEntry.data[0].length) {
|
||||
// the first data piece must be deleted
|
||||
this._totalDataLength -= firstEntry.data[0].length;
|
||||
firstEntry.data.shift();
|
||||
if (firstEntry.data.length === 0) {
|
||||
// the first entry must be deleted
|
||||
this._entries.shift();
|
||||
}
|
||||
} else {
|
||||
// the first data piece must be partially deleted
|
||||
firstEntry.data[0] = firstEntry.data[0].substr(remainingToDelete);
|
||||
this._totalDataLength -= remainingToDelete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public generateReplayEvent(): IPtyHostProcessReplayEvent {
|
||||
// normalize entries to one element per data array
|
||||
this._entries.forEach((entry) => {
|
||||
if (entry.data.length > 0) {
|
||||
entry.data = [entry.data.join('')];
|
||||
}
|
||||
});
|
||||
return {
|
||||
events: this._entries.map(entry => ({ cols: entry.cols, rows: entry.rows, data: entry.data[0] ?? '' }))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc';
|
||||
import { IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
|
||||
|
||||
enum Constants {
|
||||
MaxRestarts = 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the last terminal ID from the pty host so we can give it to the new pty host if it's
|
||||
* restarted and avoid ID conflicts.
|
||||
*/
|
||||
let lastPtyId = 0;
|
||||
|
||||
export class LocalPtyService extends Disposable implements IPtyService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private _client: Client;
|
||||
// ProxyChannel is not used here because events get lost when forwarding across multiple proxies
|
||||
private _proxy: IPtyService;
|
||||
|
||||
private _restartCount = 0;
|
||||
private _isDisposed = false;
|
||||
|
||||
private _heartbeatFirstTimeout?: NodeJS.Timeout;
|
||||
private _heartbeatSecondTimeout?: NodeJS.Timeout;
|
||||
|
||||
private readonly _onPtyHostExit = this._register(new Emitter<number>());
|
||||
readonly onPtyHostExit = this._onPtyHostExit.event;
|
||||
private readonly _onPtyHostStart = this._register(new Emitter<void>());
|
||||
readonly onPtyHostStart = this._onPtyHostStart.event;
|
||||
private readonly _onPtyHostUnresponsive = this._register(new Emitter<void>());
|
||||
readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event;
|
||||
private readonly _onPtyHostResponsive = this._register(new Emitter<void>());
|
||||
readonly onPtyHostResponsive = this._onPtyHostResponsive.event;
|
||||
|
||||
private readonly _onProcessData = this._register(new Emitter<{ id: number, event: IProcessDataEvent | string }>());
|
||||
readonly onProcessData = this._onProcessData.event;
|
||||
private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>());
|
||||
readonly onProcessExit = this._onProcessExit.event;
|
||||
private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string } }>());
|
||||
readonly onProcessReady = this._onProcessReady.event;
|
||||
private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>());
|
||||
readonly onProcessReplay = this._onProcessReplay.event;
|
||||
private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>());
|
||||
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
|
||||
private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>());
|
||||
readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event;
|
||||
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>());
|
||||
readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(toDisposable(() => this._disposePtyHost()));
|
||||
|
||||
[this._client, this._proxy] = this._startPtyHost();
|
||||
}
|
||||
|
||||
private _startPtyHost(): [Client, IPtyService] {
|
||||
const client = this._register(new Client(
|
||||
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
|
||||
{
|
||||
serverName: 'Pty Host',
|
||||
args: ['--type=ptyHost'],
|
||||
env: {
|
||||
VSCODE_LAST_PTY_ID: lastPtyId,
|
||||
VSCODE_AMD_ENTRYPOINT: 'vs/platform/terminal/node/ptyHostMain',
|
||||
VSCODE_PIPE_LOGGING: 'true',
|
||||
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
|
||||
}
|
||||
}
|
||||
));
|
||||
this._onPtyHostStart.fire();
|
||||
|
||||
const heartbeatService = ProxyChannel.toService<IHeartbeatService>(client.getChannel(TerminalIpcChannels.Heartbeat));
|
||||
heartbeatService.onBeat(() => this._handleHeartbeat());
|
||||
|
||||
// Handle exit
|
||||
this._register(client.onDidProcessExit(e => {
|
||||
this._onPtyHostExit.fire(e.code);
|
||||
if (!this._isDisposed) {
|
||||
if (this._restartCount <= Constants.MaxRestarts) {
|
||||
this._logService.error(`ptyHost terminated unexpectedly with code ${e.code}`);
|
||||
this._restartCount++;
|
||||
this.restartPtyHost();
|
||||
} else {
|
||||
this._logService.error(`ptyHost terminated unexpectedly with code ${e.code}, giving up`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Setup logging
|
||||
const logChannel = client.getChannel(TerminalIpcChannels.Log);
|
||||
this._register(this._logService.onDidChangeLogLevel(() => {
|
||||
LogLevelChannelClient.setLevel(logChannel, this._logService.getLevel());
|
||||
}));
|
||||
|
||||
// Create proxy and forward events
|
||||
const proxy = ProxyChannel.toService<IPtyService>(client.getChannel(TerminalIpcChannels.PtyHost));
|
||||
this._register(proxy.onProcessData(e => this._onProcessData.fire(e)));
|
||||
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.onProcessOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e)));
|
||||
this._register(proxy.onProcessResolvedShellLaunchConfig(e => this._onProcessResolvedShellLaunchConfig.fire(e)));
|
||||
this._register(proxy.onProcessReplay(e => this._onProcessReplay.fire(e)));
|
||||
|
||||
return [client, proxy];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean, workspaceId: string, workspaceName: string): Promise<number> {
|
||||
const timeout = setTimeout(() => this._handleUnresponsiveCreateProcess(), HeartbeatConstants.CreateProcessTimeout);
|
||||
const id = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, shouldPersist, workspaceId, workspaceName);
|
||||
clearTimeout(timeout);
|
||||
lastPtyId = Math.max(lastPtyId, id);
|
||||
return id;
|
||||
}
|
||||
attachToProcess(id: number): Promise<void> {
|
||||
return this._proxy.attachToProcess(id);
|
||||
}
|
||||
detachFromProcess(id: number): Promise<void> {
|
||||
return this._proxy.detachFromProcess(id);
|
||||
}
|
||||
|
||||
start(id: number): Promise<ITerminalLaunchError | undefined> {
|
||||
return this._proxy.start(id);
|
||||
}
|
||||
shutdown(id: number, immediate: boolean): Promise<void> {
|
||||
return this._proxy.shutdown(id, immediate);
|
||||
}
|
||||
input(id: number, data: string): Promise<void> {
|
||||
return this._proxy.input(id, data);
|
||||
}
|
||||
resize(id: number, cols: number, rows: number): Promise<void> {
|
||||
return this._proxy.resize(id, cols, rows);
|
||||
}
|
||||
acknowledgeDataEvent(id: number, charCount: number): Promise<void> {
|
||||
return this._proxy.acknowledgeDataEvent(id, charCount);
|
||||
}
|
||||
getInitialCwd(id: number): Promise<string> {
|
||||
return this._proxy.getInitialCwd(id);
|
||||
}
|
||||
getCwd(id: number): Promise<string> {
|
||||
return this._proxy.getCwd(id);
|
||||
}
|
||||
getLatency(id: number): Promise<number> {
|
||||
return this._proxy.getLatency(id);
|
||||
}
|
||||
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): void {
|
||||
return this._proxy.setTerminalLayoutInfo(args);
|
||||
}
|
||||
async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined> {
|
||||
return await this._proxy.getTerminalLayoutInfo(args);
|
||||
}
|
||||
|
||||
async restartPtyHost(): Promise<void> {
|
||||
this._disposePtyHost();
|
||||
[this._client, this._proxy] = this._startPtyHost();
|
||||
}
|
||||
|
||||
private _disposePtyHost(): void {
|
||||
if (this._proxy.shutdownAll) {
|
||||
this._proxy.shutdownAll();
|
||||
}
|
||||
this._client.dispose();
|
||||
}
|
||||
|
||||
private _handleHeartbeat() {
|
||||
this._clearHeartbeatTimeouts();
|
||||
this._heartbeatFirstTimeout = setTimeout(() => this._handleHeartbeatFirstTimeout(), HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier);
|
||||
this._onPtyHostResponsive.fire();
|
||||
}
|
||||
|
||||
private _handleHeartbeatFirstTimeout() {
|
||||
this._logService.warn(`No ptyHost heartbeat after ${HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier / 1000} seconds`);
|
||||
this._heartbeatFirstTimeout = undefined;
|
||||
this._heartbeatSecondTimeout = setTimeout(() => this._handleHeartbeatSecondTimeout(), HeartbeatConstants.BeatInterval * HeartbeatConstants.SecondWaitMultiplier);
|
||||
}
|
||||
|
||||
private _handleHeartbeatSecondTimeout() {
|
||||
this._logService.error(`No ptyHost heartbeat after ${(HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier + HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier) / 1000} seconds`);
|
||||
this._heartbeatSecondTimeout = undefined;
|
||||
this._onPtyHostUnresponsive.fire();
|
||||
}
|
||||
|
||||
private _handleUnresponsiveCreateProcess() {
|
||||
this._clearHeartbeatTimeouts();
|
||||
this._logService.error(`No ptyHost response to createProcess after ${HeartbeatConstants.CreateProcessTimeout / 1000} seconds`);
|
||||
this._onPtyHostUnresponsive.fire();
|
||||
}
|
||||
|
||||
private _clearHeartbeatTimeouts() {
|
||||
if (this._heartbeatFirstTimeout) {
|
||||
clearTimeout(this._heartbeatFirstTimeout);
|
||||
this._heartbeatFirstTimeout = undefined;
|
||||
}
|
||||
if (this._heartbeatSecondTimeout) {
|
||||
clearTimeout(this._heartbeatSecondTimeout);
|
||||
this._heartbeatSecondTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { }
|
||||
22
lib/vscode/src/vs/platform/terminal/node/heartbeatService.ts
Normal file
22
lib/vscode/src/vs/platform/terminal/node/heartbeatService.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { HeartbeatConstants, IHeartbeatService } from 'vs/platform/terminal/common/terminal';
|
||||
|
||||
export class HeartbeatService extends Disposable implements IHeartbeatService {
|
||||
private readonly _onBeat = this._register(new Emitter<void>());
|
||||
readonly onBeat = this._onBeat.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
this._onBeat.fire();
|
||||
}, HeartbeatConstants.BeatInterval);
|
||||
this._register(toDisposable(() => clearInterval(interval)));
|
||||
}
|
||||
}
|
||||
33
lib/vscode/src/vs/platform/terminal/node/ptyHostMain.ts
Normal file
33
lib/vscode/src/vs/platform/terminal/node/ptyHostMain.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { PtyService } from 'vs/platform/terminal/node/ptyService';
|
||||
import { TerminalIpcChannels } from 'vs/platform/terminal/common/terminal';
|
||||
import { ConsoleLogger, LogService } from 'vs/platform/log/common/log';
|
||||
import { LogLevelChannel } from 'vs/platform/log/common/logIpc';
|
||||
import { HeartbeatService } from 'vs/platform/terminal/node/heartbeatService';
|
||||
|
||||
const server = new Server('ptyHost');
|
||||
|
||||
const lastPtyId = parseInt(process.env.VSCODE_LAST_PTY_ID || '0');
|
||||
delete process.env.VSCODE_LAST_PTY_ID;
|
||||
|
||||
const logService = new LogService(new ConsoleLogger());
|
||||
const logChannel = new LogLevelChannel(logService);
|
||||
server.registerChannel(TerminalIpcChannels.Log, logChannel);
|
||||
|
||||
const heartbeatService = new HeartbeatService();
|
||||
server.registerChannel(TerminalIpcChannels.Heartbeat, ProxyChannel.fromService(heartbeatService));
|
||||
|
||||
const ptyService = new PtyService(lastPtyId, logService);
|
||||
server.registerChannel(TerminalIpcChannels.PtyHost, ProxyChannel.fromService(ptyService));
|
||||
|
||||
process.once('exit', () => {
|
||||
logService.dispose();
|
||||
heartbeatService.dispose();
|
||||
ptyService.dispose();
|
||||
});
|
||||
426
lib/vscode/src/vs/platform/terminal/node/ptyService.ts
Normal file
426
lib/vscode/src/vs/platform/terminal/node/ptyService.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder';
|
||||
import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess';
|
||||
import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IPtyHostDescriptionDto, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
type WorkspaceId = string;
|
||||
|
||||
export class PtyService extends Disposable implements IPtyService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _ptys: Map<number, PersistentTerminalProcess> = new Map();
|
||||
private readonly _workspaceLayoutInfos = new Map<WorkspaceId, ISetTerminalLayoutInfoArgs>();
|
||||
|
||||
private readonly _onHeartbeat = this._register(new Emitter<void>());
|
||||
readonly onHeartbeat = this._onHeartbeat.event;
|
||||
|
||||
private readonly _onProcessData = this._register(new Emitter<{ id: number, event: IProcessDataEvent | string }>());
|
||||
readonly onProcessData = this._onProcessData.event;
|
||||
private readonly _onProcessReplay = this._register(new Emitter<{ id: number, event: IPtyHostProcessReplayEvent }>());
|
||||
readonly onProcessReplay = this._onProcessReplay.event;
|
||||
private readonly _onProcessExit = this._register(new Emitter<{ id: number, event: number | undefined }>());
|
||||
readonly onProcessExit = this._onProcessExit.event;
|
||||
private readonly _onProcessReady = this._register(new Emitter<{ id: number, event: { pid: number, cwd: string } }>());
|
||||
readonly onProcessReady = this._onProcessReady.event;
|
||||
private readonly _onProcessTitleChanged = this._register(new Emitter<{ id: number, event: string }>());
|
||||
readonly onProcessTitleChanged = this._onProcessTitleChanged.event;
|
||||
private readonly _onProcessOverrideDimensions = this._register(new Emitter<{ id: number, event: ITerminalDimensionsOverride | undefined }>());
|
||||
readonly onProcessOverrideDimensions = this._onProcessOverrideDimensions.event;
|
||||
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<{ id: number, event: IShellLaunchConfig }>());
|
||||
readonly onProcessResolvedShellLaunchConfig = this._onProcessResolvedShellLaunchConfig.event;
|
||||
|
||||
constructor(
|
||||
private _lastPtyId: number,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(toDisposable(() => {
|
||||
for (const pty of this._ptys.values()) {
|
||||
pty.shutdown(true);
|
||||
}
|
||||
this._ptys.clear();
|
||||
}));
|
||||
}
|
||||
|
||||
async shutdownAll(): Promise<void> {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
async createProcess(
|
||||
shellLaunchConfig: IShellLaunchConfig,
|
||||
cwd: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
env: IProcessEnvironment,
|
||||
executableEnv: IProcessEnvironment,
|
||||
windowsEnableConpty: boolean,
|
||||
shouldPersist: boolean,
|
||||
workspaceId: string,
|
||||
workspaceName: string
|
||||
): Promise<number> {
|
||||
if (shellLaunchConfig.attachPersistentTerminal) {
|
||||
throw new Error('Attempt to create a process when attach object was provided');
|
||||
}
|
||||
const id = ++this._lastPtyId;
|
||||
const process = new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, this._logService);
|
||||
process.onProcessData(event => this._onProcessData.fire({ id, event }));
|
||||
process.onProcessExit(event => this._onProcessExit.fire({ id, event }));
|
||||
if (process.onProcessOverrideDimensions) {
|
||||
process.onProcessOverrideDimensions(event => this._onProcessOverrideDimensions.fire({ id, event }));
|
||||
}
|
||||
if (process.onProcessResolvedShellLaunchConfig) {
|
||||
process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event }));
|
||||
}
|
||||
const persistentTerminalProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService);
|
||||
process.onProcessExit(() => {
|
||||
persistentTerminalProcess.dispose();
|
||||
this._ptys.delete(id);
|
||||
});
|
||||
persistentTerminalProcess.onProcessReplay(event => this._onProcessReplay.fire({ id, event }));
|
||||
persistentTerminalProcess.onProcessReady(event => this._onProcessReady.fire({ id, event }));
|
||||
persistentTerminalProcess.onProcessTitleChanged(event => this._onProcessTitleChanged.fire({ id, event }));
|
||||
this._ptys.set(id, persistentTerminalProcess);
|
||||
return id;
|
||||
}
|
||||
|
||||
async attachToProcess(id: number): Promise<void> {
|
||||
try {
|
||||
this._throwIfNoPty(id).attach();
|
||||
this._logService.trace(`Persistent terminal reconnection "${id}"`);
|
||||
} catch (e) {
|
||||
this._logService.trace(`Persistent terminal reconnection "${id}" failed`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async detachFromProcess(id: number): Promise<void> {
|
||||
this._throwIfNoPty(id).detach();
|
||||
}
|
||||
|
||||
async start(id: number): Promise<ITerminalLaunchError | undefined> {
|
||||
return this._throwIfNoPty(id).start();
|
||||
}
|
||||
async shutdown(id: number, immediate: boolean): Promise<void> {
|
||||
return this._throwIfNoPty(id).shutdown(immediate);
|
||||
}
|
||||
async input(id: number, data: string): Promise<void> {
|
||||
return this._throwIfNoPty(id).input(data);
|
||||
}
|
||||
async resize(id: number, cols: number, rows: number): Promise<void> {
|
||||
return this._throwIfNoPty(id).resize(cols, rows);
|
||||
}
|
||||
async getInitialCwd(id: number): Promise<string> {
|
||||
return this._throwIfNoPty(id).getInitialCwd();
|
||||
}
|
||||
async getCwd(id: number): Promise<string> {
|
||||
return this._throwIfNoPty(id).getCwd();
|
||||
}
|
||||
async acknowledgeDataEvent(id: number, charCount: number): Promise<void> {
|
||||
return this._throwIfNoPty(id).acknowledgeDataEvent(charCount);
|
||||
}
|
||||
async getLatency(id: number): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
|
||||
this._workspaceLayoutInfos.set(args.workspaceId, args);
|
||||
}
|
||||
|
||||
async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined> {
|
||||
const layout = this._workspaceLayoutInfos.get(args.workspaceId);
|
||||
if (layout) {
|
||||
const expandedTabs = await Promise.all(layout.tabs.map(async tab => this._expandTerminalTab(tab)));
|
||||
const filtered = expandedTabs.filter(t => t.terminals.length > 0);
|
||||
return {
|
||||
tabs: filtered
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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>[];
|
||||
return {
|
||||
isActive: tab.isActive,
|
||||
activePersistentTerminalId: tab.activePersistentTerminalId,
|
||||
terminals: filtered
|
||||
};
|
||||
}
|
||||
|
||||
private async _expandTerminalInstance(t: ITerminalInstanceLayoutInfoById): Promise<IRawTerminalInstanceLayoutInfo<IPtyHostDescriptionDto | null>> {
|
||||
try {
|
||||
const persistentTerminalProcess = this._throwIfNoPty(t.terminal);
|
||||
const termDto = persistentTerminalProcess && await this._terminalToDto(t.terminal, persistentTerminalProcess);
|
||||
return {
|
||||
terminal: termDto ?? null,
|
||||
relativeSize: t.relativeSize
|
||||
};
|
||||
} catch (e) {
|
||||
this._logService.trace(`Couldn't get layout info, a terminal was probably disconnected`, e.message);
|
||||
// this will be filtered out and not reconnected
|
||||
return {
|
||||
terminal: null,
|
||||
relativeSize: t.relativeSize
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async _terminalToDto(id: number, persistentTerminalProcess: PersistentTerminalProcess): Promise<IPtyHostDescriptionDto> {
|
||||
const [cwd, isOrphan] = await Promise.all([persistentTerminalProcess.getCwd(), persistentTerminalProcess.isOrphaned()]);
|
||||
return {
|
||||
id,
|
||||
title: persistentTerminalProcess.title,
|
||||
pid: persistentTerminalProcess.pid,
|
||||
workspaceId: persistentTerminalProcess.workspaceId,
|
||||
workspaceName: persistentTerminalProcess.workspaceName,
|
||||
cwd,
|
||||
isOrphan
|
||||
};
|
||||
}
|
||||
|
||||
private _throwIfNoPty(id: number): PersistentTerminalProcess {
|
||||
const pty = this._ptys.get(id);
|
||||
if (!pty) {
|
||||
throw new Error(`Could not find pty with id "${id}"`);
|
||||
}
|
||||
return pty;
|
||||
}
|
||||
}
|
||||
|
||||
export class PersistentTerminalProcess extends Disposable {
|
||||
|
||||
// private readonly _bufferer: TerminalDataBufferer;
|
||||
|
||||
private readonly _pendingCommands = new Map<number, { resolve: (data: any) => void; reject: (err: any) => void; }>();
|
||||
|
||||
private readonly _recorder: TerminalRecorder;
|
||||
private _isStarted: boolean = false;
|
||||
|
||||
private _orphanQuestionBarrier: AutoOpenBarrier | null;
|
||||
private _orphanQuestionReplyTime: number;
|
||||
private _orphanRequestQueue = new Queue<boolean>();
|
||||
private _disconnectRunner1: RunOnceScheduler;
|
||||
private _disconnectRunner2: RunOnceScheduler;
|
||||
|
||||
private readonly _onProcessReplay = this._register(new Emitter<IPtyHostProcessReplayEvent>());
|
||||
readonly onProcessReplay = this._onProcessReplay.event;
|
||||
private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>());
|
||||
readonly onProcessReady = this._onProcessReady.event;
|
||||
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
|
||||
readonly onProcessTitleChanged = this._onProcessTitleChanged.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 _inReplay = false;
|
||||
|
||||
private _pid = -1;
|
||||
private _cwd = '';
|
||||
|
||||
get pid(): number { return this._pid; }
|
||||
get title(): string { return this._terminalProcess.currentTitle; }
|
||||
|
||||
constructor(
|
||||
private _persistentTerminalId: number,
|
||||
private readonly _terminalProcess: TerminalProcess,
|
||||
public readonly workspaceId: string,
|
||||
public readonly workspaceName: string,
|
||||
public readonly shouldPersistTerminal: boolean,
|
||||
cols: number, rows: number,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this._recorder = new TerminalRecorder(cols, rows);
|
||||
this._orphanQuestionBarrier = null;
|
||||
this._orphanQuestionReplyTime = 0;
|
||||
this._disconnectRunner1 = this._register(new RunOnceScheduler(() => {
|
||||
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`);
|
||||
this.shutdown(true);
|
||||
}, LocalReconnectConstants.ReconnectionGraceTime));
|
||||
this._disconnectRunner2 = this._register(new RunOnceScheduler(() => {
|
||||
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": The short reconnection grace time of ${printTime(LocalReconnectConstants.ReconnectionShortGraceTime)} has expired, so the process (pid=${this._pid}) will be shutdown.`);
|
||||
this.shutdown(true);
|
||||
}, LocalReconnectConstants.ReconnectionShortGraceTime));
|
||||
|
||||
// TODO: Bring back bufferer
|
||||
// this._bufferer = new TerminalDataBufferer((id, data) => {
|
||||
// const ev: IPtyHostProcessDataEvent = {
|
||||
// type: 'data',
|
||||
// data: data
|
||||
// };
|
||||
// this._events.fire(ev);
|
||||
// });
|
||||
|
||||
this._register(this._terminalProcess.onProcessReady(e => {
|
||||
this._pid = e.pid;
|
||||
this._cwd = e.cwd;
|
||||
this._onProcessReady.fire(e);
|
||||
}));
|
||||
this._register(this._terminalProcess.onProcessTitleChanged(e => this._onProcessTitleChanged.fire(e)));
|
||||
|
||||
// Buffer data events to reduce the amount of messages going to the renderer
|
||||
// this._register(this._bufferer.startBuffering(this._persistentTerminalId, this._terminalProcess.onProcessData));
|
||||
this._register(this._terminalProcess.onProcessData(e => this._recorder.recordData(e)));
|
||||
this._register(this._terminalProcess.onProcessExit(exitCode => {
|
||||
// this._bufferer.stopBuffering(this._persistentTerminalId);
|
||||
}));
|
||||
}
|
||||
|
||||
attach(): void {
|
||||
this._disconnectRunner1.cancel();
|
||||
}
|
||||
|
||||
async detach(): Promise<void> {
|
||||
if (this.shouldPersistTerminal) {
|
||||
this._disconnectRunner1.schedule();
|
||||
} else {
|
||||
this.shutdown(true);
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<ITerminalLaunchError | undefined> {
|
||||
if (!this._isStarted) {
|
||||
const result = await this._terminalProcess.start();
|
||||
if (result) {
|
||||
// it's a terminal launch error
|
||||
return result;
|
||||
}
|
||||
this._isStarted = true;
|
||||
} else {
|
||||
this._onProcessReady.fire({ pid: this._pid, cwd: this._cwd });
|
||||
this._onProcessTitleChanged.fire(this._terminalProcess.currentTitle);
|
||||
this.triggerReplay();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
shutdown(immediate: boolean): void {
|
||||
return this._terminalProcess.shutdown(immediate);
|
||||
}
|
||||
input(data: string): void {
|
||||
if (this._inReplay) {
|
||||
return;
|
||||
}
|
||||
return this._terminalProcess.input(data);
|
||||
}
|
||||
resize(cols: number, rows: number): void {
|
||||
if (this._inReplay) {
|
||||
return;
|
||||
}
|
||||
this._recorder.recordResize(cols, rows);
|
||||
return this._terminalProcess.resize(cols, rows);
|
||||
}
|
||||
acknowledgeDataEvent(charCount: number): void {
|
||||
if (this._inReplay) {
|
||||
return;
|
||||
}
|
||||
return this._terminalProcess.acknowledgeDataEvent(charCount);
|
||||
}
|
||||
getInitialCwd(): Promise<string> {
|
||||
return this._terminalProcess.getInitialCwd();
|
||||
}
|
||||
getCwd(): Promise<string> {
|
||||
return this._terminalProcess.getCwd();
|
||||
}
|
||||
getLatency(): Promise<number> {
|
||||
return this._terminalProcess.getLatency();
|
||||
}
|
||||
|
||||
triggerReplay(): void {
|
||||
const ev = this._recorder.generateReplayEvent();
|
||||
let dataLength = 0;
|
||||
for (const e of ev.events) {
|
||||
dataLength += e.data.length;
|
||||
}
|
||||
|
||||
this._logService.info(`Persistent terminal "${this._persistentTerminalId}": Replaying ${dataLength} chars and ${ev.events.length} size events`);
|
||||
this._onProcessReplay.fire(ev);
|
||||
this._terminalProcess.clearUnacknowledgedChars();
|
||||
}
|
||||
|
||||
sendCommandResult(reqId: number, isError: boolean, serializedPayload: any): void {
|
||||
const data = this._pendingCommands.get(reqId);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
this._pendingCommands.delete(reqId);
|
||||
}
|
||||
|
||||
async orphanQuestionReply(): Promise<void> {
|
||||
this._orphanQuestionReplyTime = Date.now();
|
||||
if (this._orphanQuestionBarrier) {
|
||||
const barrier = this._orphanQuestionBarrier;
|
||||
this._orphanQuestionBarrier = null;
|
||||
barrier.open();
|
||||
}
|
||||
}
|
||||
|
||||
reduceGraceTime(): void {
|
||||
if (this._disconnectRunner2.isScheduled()) {
|
||||
// we are disconnected and already running the short reconnection timer
|
||||
return;
|
||||
}
|
||||
if (this._disconnectRunner1.isScheduled()) {
|
||||
// we are disconnected and running the long reconnection timer
|
||||
this._disconnectRunner2.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
async isOrphaned(): Promise<boolean> {
|
||||
return await this._orphanRequestQueue.queue(async () => this._isOrphaned());
|
||||
}
|
||||
|
||||
private async _isOrphaned(): Promise<boolean> {
|
||||
if (this._disconnectRunner1.isScheduled() || this._disconnectRunner2.isScheduled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this._orphanQuestionBarrier) {
|
||||
// the barrier opens after 4 seconds with or without a reply
|
||||
this._orphanQuestionBarrier = new AutoOpenBarrier(4000);
|
||||
this._orphanQuestionReplyTime = 0;
|
||||
// TODO: Fire?
|
||||
// const ev: IPtyHostProcessOrphanQuestionEvent = {
|
||||
// type: 'orphan?'
|
||||
// };
|
||||
// this._events.fire(ev);
|
||||
}
|
||||
|
||||
await this._orphanQuestionBarrier.wait();
|
||||
return (Date.now() - this._orphanQuestionReplyTime > 500);
|
||||
}
|
||||
}
|
||||
|
||||
function printTime(ms: number): string {
|
||||
let h = 0;
|
||||
let m = 0;
|
||||
let s = 0;
|
||||
if (ms >= 1000) {
|
||||
s = Math.floor(ms / 1000);
|
||||
ms -= s * 1000;
|
||||
}
|
||||
if (s >= 60) {
|
||||
m = Math.floor(s / 60);
|
||||
s -= m * 60;
|
||||
}
|
||||
if (m >= 60) {
|
||||
h = Math.floor(m / 60);
|
||||
m -= h * 60;
|
||||
}
|
||||
const _h = h ? `${h}h` : ``;
|
||||
const _m = m ? `${m}m` : ``;
|
||||
const _s = s ? `${s}s` : ``;
|
||||
const _ms = ms ? `${ms}ms` : ``;
|
||||
return `${_h}${_m}${_s}${_ms}`;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { exists } from 'vs/base/node/pfs';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { getCaseInsensitive } from 'vs/base/common/objects';
|
||||
import { IProcessEnvironment, isWindows } from 'vs/base/common/platform';
|
||||
|
||||
export function getWindowsBuildNumber(): number {
|
||||
const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release());
|
||||
let buildNumber: number = 0;
|
||||
if (osVersion && osVersion.length === 4) {
|
||||
buildNumber = parseInt(osVersion[3]);
|
||||
}
|
||||
return buildNumber;
|
||||
}
|
||||
|
||||
export async function findExecutable(command: string, cwd?: string, paths?: string[], env: IProcessEnvironment = process.env as IProcessEnvironment): Promise<string | undefined> {
|
||||
// If we have an absolute path then we take it.
|
||||
if (path.isAbsolute(command)) {
|
||||
return await exists(command) ? command : undefined;
|
||||
}
|
||||
if (cwd === undefined) {
|
||||
cwd = process.cwd();
|
||||
}
|
||||
const dir = path.dirname(command);
|
||||
if (dir !== '.') {
|
||||
// We have a directory and the directory is relative (see above). Make the path absolute
|
||||
// to the current working directory.
|
||||
const fullPath = path.join(cwd, command);
|
||||
return await exists(fullPath) ? fullPath : undefined;
|
||||
}
|
||||
const envPath = getCaseInsensitive(env, 'PATH');
|
||||
if (paths === undefined && isString(envPath)) {
|
||||
paths = envPath.split(path.delimiter);
|
||||
}
|
||||
// No PATH environment. Make path absolute to the cwd.
|
||||
if (paths === undefined || paths.length === 0) {
|
||||
const fullPath = path.join(cwd, command);
|
||||
return await exists(fullPath) ? fullPath : undefined;
|
||||
}
|
||||
// We have a simple file name. We get the path variable from the env
|
||||
// and try to find the executable on the path.
|
||||
for (let pathEntry of paths) {
|
||||
// The path entry is absolute.
|
||||
let fullPath: string;
|
||||
if (path.isAbsolute(pathEntry)) {
|
||||
fullPath = path.join(pathEntry, command);
|
||||
} else {
|
||||
fullPath = path.join(cwd, pathEntry, command);
|
||||
}
|
||||
|
||||
if (await exists(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
if (isWindows) {
|
||||
let withExtension = fullPath + '.com';
|
||||
if (await exists(withExtension)) {
|
||||
return withExtension;
|
||||
}
|
||||
withExtension = fullPath + '.exe';
|
||||
if (await exists(withExtension)) {
|
||||
return withExtension;
|
||||
}
|
||||
}
|
||||
}
|
||||
const fullPath = path.join(cwd, command);
|
||||
return await exists(fullPath) ? fullPath : undefined;
|
||||
}
|
||||
479
lib/vscode/src/vs/platform/terminal/node/terminalProcess.ts
Normal file
479
lib/vscode/src/vs/platform/terminal/node/terminalProcess.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import type * as pty from 'node-pty';
|
||||
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 { 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';
|
||||
|
||||
// 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
|
||||
// hardware is. The workaround for this is to space out when large amounts of data is being written
|
||||
// to the terminal. See https://github.com/microsoft/vscode/issues/38137
|
||||
const WRITE_MAX_CHUNK_SIZE = 50;
|
||||
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
|
||||
* 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
|
||||
* flush to hang the pty host][2] because [conhost should be hosted on another thread][3].
|
||||
*
|
||||
* [1]: https://github.com/Tyriar/node-pty/issues/72
|
||||
* [2]: https://github.com/microsoft/vscode/issues/71966
|
||||
* [3]: https://github.com/microsoft/node-pty/pull/415
|
||||
*/
|
||||
DataFlushTimeout = 250,
|
||||
/**
|
||||
* The maximum time to allow after dispose is called because forcefully killing the process.
|
||||
*/
|
||||
MaximumShutdownTime = 5000
|
||||
}
|
||||
|
||||
export class TerminalProcess extends Disposable implements ITerminalChildProcess {
|
||||
readonly id = 0;
|
||||
readonly shouldPersist = false;
|
||||
|
||||
private _exitCode: number | undefined;
|
||||
private _exitMessage: string | undefined;
|
||||
private _closeTimeout: any;
|
||||
private _ptyProcess: pty.IPty | undefined;
|
||||
private _currentTitle: string = '';
|
||||
private _processStartupComplete: Promise<void> | undefined;
|
||||
private _isDisposed: boolean = false;
|
||||
private _titleInterval: NodeJS.Timer | null = null;
|
||||
private _writeQueue: string[] = [];
|
||||
private _writeTimeout: NodeJS.Timeout | undefined;
|
||||
private _delayedResizer: DelayedResizer | undefined;
|
||||
private readonly _initialCwd: string;
|
||||
private readonly _ptyOptions: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions;
|
||||
|
||||
private _isPtyPaused: boolean = false;
|
||||
private _unacknowledgedCharCount: number = 0;
|
||||
|
||||
public get exitMessage(): string | undefined { return this._exitMessage; }
|
||||
public get currentTitle(): string { return this._currentTitle; }
|
||||
|
||||
private readonly _onProcessData = this._register(new Emitter<string>());
|
||||
public get onProcessData(): Event<string> { return this._onProcessData.event; }
|
||||
private readonly _onProcessExit = this._register(new Emitter<number>());
|
||||
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
|
||||
private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>());
|
||||
public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; }
|
||||
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
|
||||
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
|
||||
|
||||
constructor(
|
||||
private readonly _shellLaunchConfig: IShellLaunchConfig,
|
||||
cwd: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
env: platform.IProcessEnvironment,
|
||||
/**
|
||||
* environment used for `findExecutable`
|
||||
*/
|
||||
private readonly _executableEnv: platform.IProcessEnvironment,
|
||||
windowsEnableConpty: boolean,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
let name: string;
|
||||
if (platform.isWindows) {
|
||||
name = path.basename(this._shellLaunchConfig.executable || '');
|
||||
} else {
|
||||
// Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a
|
||||
// color prompt as defined in the default ~/.bashrc file.
|
||||
name = 'xterm-256color';
|
||||
}
|
||||
this._initialCwd = cwd;
|
||||
const useConpty = windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309;
|
||||
this._ptyOptions = {
|
||||
name,
|
||||
cwd,
|
||||
env,
|
||||
cols,
|
||||
rows,
|
||||
useConpty,
|
||||
// This option will force conpty to not redraw the whole viewport on launch
|
||||
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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
onProcessOverrideDimensions?: Event<ITerminalDimensionsOverride | undefined> | undefined;
|
||||
onProcessResolvedShellLaunchConfig?: Event<IShellLaunchConfig> | undefined;
|
||||
|
||||
public async start(): Promise<ITerminalLaunchError | undefined> {
|
||||
const results = await Promise.all([this._validateCwd(), this._validateExecutable()]);
|
||||
const firstError = results.find(r => r !== undefined);
|
||||
if (firstError) {
|
||||
return firstError;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions);
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
this._logService.trace('IPty#spawn native exception', err);
|
||||
return { message: `A native exception occurred during launch (${err.message})` };
|
||||
}
|
||||
}
|
||||
|
||||
private async _validateCwd(): Promise<undefined | ITerminalLaunchError> {
|
||||
try {
|
||||
const result = await fs.promises.stat(this._initialCwd);
|
||||
if (!result.isDirectory()) {
|
||||
return { message: localize('launchFail.cwdNotDirectory', "Starting directory (cwd) \"{0}\" is not a directory", this._initialCwd.toString()) };
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.code === 'ENOENT') {
|
||||
return { message: localize('launchFail.cwdDoesNotExist', "Starting directory (cwd) \"{0}\" does not exist", this._initialCwd.toString()) };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _validateExecutable(): Promise<undefined | ITerminalLaunchError> {
|
||||
const slc = this._shellLaunchConfig;
|
||||
if (!slc.executable) {
|
||||
throw new Error('IShellLaunchConfig.executable not set');
|
||||
}
|
||||
try {
|
||||
const result = await fs.promises.stat(slc.executable);
|
||||
if (!result.isFile() && !result.isSymbolicLink()) {
|
||||
return { message: localize('launchFail.executableIsNotFileOrSymlink', "Path to shell executable \"{0}\" is not a file of a symlink", slc.executable) };
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.code === 'ENOENT') {
|
||||
// The executable isn't an absolute path, try find it on the PATH or CWD
|
||||
let cwd = slc.cwd instanceof URI ? slc.cwd.path : slc.cwd!;
|
||||
const envPaths: string[] | undefined = (slc.env && slc.env.PATH) ? slc.env.PATH.split(path.delimiter) : undefined;
|
||||
const executable = await findExecutable(slc.executable!, cwd, envPaths, this._executableEnv);
|
||||
if (!executable) {
|
||||
return { message: localize('launchFail.executableDoesNotExist', "Path to shell executable \"{0}\" does not exist", slc.executable) };
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): Promise<void> {
|
||||
const args = shellLaunchConfig.args || [];
|
||||
this._logService.trace('IPty#spawn', shellLaunchConfig.executable, args, options);
|
||||
const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options);
|
||||
this._ptyProcess = ptyProcess;
|
||||
this._processStartupComplete = new Promise<void>(c => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
this._onProcessData.fire(data);
|
||||
if (this._closeTimeout) {
|
||||
clearTimeout(this._closeTimeout);
|
||||
this._queueProcessExit();
|
||||
}
|
||||
});
|
||||
ptyProcess.onExit(e => {
|
||||
this._exitCode = e.exitCode;
|
||||
this._queueProcessExit();
|
||||
});
|
||||
this._setupTitlePolling(ptyProcess);
|
||||
this._sendProcessId(ptyProcess.pid);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
if (this._titleInterval) {
|
||||
clearInterval(this._titleInterval);
|
||||
}
|
||||
this._titleInterval = null;
|
||||
this._onProcessData.dispose();
|
||||
this._onProcessExit.dispose();
|
||||
this._onProcessReady.dispose();
|
||||
this._onProcessTitleChanged.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _setupTitlePolling(ptyProcess: pty.IPty) {
|
||||
// Send initial timeout async to give event listeners a chance to init
|
||||
setTimeout(() => this._sendProcessTitle(ptyProcess), 0);
|
||||
// Setup polling for non-Windows, for Windows `process` doesn't change
|
||||
if (!platform.isWindows) {
|
||||
this._titleInterval = setInterval(() => {
|
||||
if (this._currentTitle !== ptyProcess.process) {
|
||||
this._sendProcessTitle(ptyProcess);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow any trailing data events to be sent before the exit event is sent.
|
||||
// See https://github.com/Tyriar/node-pty/issues/72
|
||||
private _queueProcessExit() {
|
||||
if (this._closeTimeout) {
|
||||
clearTimeout(this._closeTimeout);
|
||||
}
|
||||
this._closeTimeout = setTimeout(() => {
|
||||
this._closeTimeout = undefined;
|
||||
this._kill();
|
||||
}, ShutdownConstants.DataFlushTimeout);
|
||||
}
|
||||
|
||||
private async _kill(): Promise<void> {
|
||||
// Wait to kill to process until the start up code has run. This prevents us from firing a process exit before a
|
||||
// process start.
|
||||
await this._processStartupComplete;
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
// Attempt to kill the pty, it may have already been killed at this
|
||||
// point but we want to make sure
|
||||
try {
|
||||
if (this._ptyProcess) {
|
||||
this._logService.trace('IPty#kill');
|
||||
this._ptyProcess.kill();
|
||||
}
|
||||
} catch (ex) {
|
||||
// Swallow, the pty has already been killed
|
||||
}
|
||||
this._onProcessExit.fire(this._exitCode || 0);
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private _sendProcessId(pid: number) {
|
||||
this._onProcessReady.fire({ pid, cwd: this._initialCwd });
|
||||
}
|
||||
|
||||
private _sendProcessTitle(ptyProcess: pty.IPty): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._currentTitle = ptyProcess.process;
|
||||
this._onProcessTitleChanged.fire(this._currentTitle);
|
||||
}
|
||||
|
||||
public async shutdown(immediate: boolean): Promise<void> {
|
||||
if (immediate) {
|
||||
await this._kill();
|
||||
} else {
|
||||
if (!this._closeTimeout && !this._isDisposed) {
|
||||
this._queueProcessExit();
|
||||
// Allow a maximum amount of time for the process to exit, otherwise force kill it
|
||||
setTimeout(() => {
|
||||
if (this._closeTimeout && !this._isDisposed) {
|
||||
this._closeTimeout = undefined;
|
||||
this._kill();
|
||||
}
|
||||
}, ShutdownConstants.MaximumShutdownTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public input(data: string): void {
|
||||
if (this._isDisposed || !this._ptyProcess) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i <= Math.floor(data.length / WRITE_MAX_CHUNK_SIZE); i++) {
|
||||
this._writeQueue.push(data.substr(i * WRITE_MAX_CHUNK_SIZE, WRITE_MAX_CHUNK_SIZE));
|
||||
}
|
||||
this._startWrite();
|
||||
}
|
||||
|
||||
private _startWrite(): void {
|
||||
// Don't write if it's already queued of is there is nothing to write
|
||||
if (this._writeTimeout !== undefined || this._writeQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._doWrite();
|
||||
|
||||
// Don't queue more writes if the queue is empty
|
||||
if (this._writeQueue.length === 0) {
|
||||
this._writeTimeout = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the next write
|
||||
this._writeTimeout = setTimeout(() => {
|
||||
this._writeTimeout = undefined;
|
||||
this._startWrite();
|
||||
}, WRITE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
private _doWrite(): void {
|
||||
const data = this._writeQueue.shift()!;
|
||||
this._logService.trace('IPty#write', `${data.length} characters`);
|
||||
this._ptyProcess!.write(data);
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
if (typeof cols !== 'number' || typeof rows !== 'number' || isNaN(cols) || isNaN(rows)) {
|
||||
return;
|
||||
}
|
||||
// Ensure that cols and rows are always >= 1, this prevents a native
|
||||
// exception in winpty.
|
||||
if (this._ptyProcess) {
|
||||
cols = Math.max(cols, 1);
|
||||
rows = Math.max(rows, 1);
|
||||
|
||||
// Delay resize if needed
|
||||
if (this._delayedResizer) {
|
||||
this._delayedResizer.cols = cols;
|
||||
this._delayedResizer.rows = rows;
|
||||
return;
|
||||
}
|
||||
|
||||
this._logService.trace('IPty#resize', cols, rows);
|
||||
try {
|
||||
this._ptyProcess.resize(cols, rows);
|
||||
} catch (e) {
|
||||
// Swallow error if the pty has already exited
|
||||
this._logService.trace('IPty#resize exception ' + e.message);
|
||||
if (this._exitCode !== undefined && e.message !== 'ioctl(2) failed, EBADF') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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})`);
|
||||
if (this._isPtyPaused && this._unacknowledgedCharCount < FlowControlConstants.LowWatermarkChars) {
|
||||
this._logService.trace(`Flow control: Resume (${this._unacknowledgedCharCount} < ${FlowControlConstants.LowWatermarkChars})`);
|
||||
this._ptyProcess?.resume();
|
||||
this._isPtyPaused = false;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
this._ptyProcess?.resume();
|
||||
this._isPtyPaused = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getInitialCwd(): Promise<string> {
|
||||
return Promise.resolve(this._initialCwd);
|
||||
}
|
||||
|
||||
public getCwd(): Promise<string> {
|
||||
if (platform.isMacintosh) {
|
||||
// Disable cwd lookup on macOS Big Sur due to spawn blocking thread (darwin v20 is macOS
|
||||
// Big Sur) https://github.com/Microsoft/vscode/issues/105446
|
||||
const osRelease = os.release().split('.');
|
||||
if (osRelease.length > 0 && parseInt(osRelease[0]) < 20) {
|
||||
return new Promise<string>(resolve => {
|
||||
if (!this._ptyProcess) {
|
||||
resolve(this._initialCwd);
|
||||
return;
|
||||
}
|
||||
this._logService.trace('IPty#pid');
|
||||
exec('lsof -OPln -p ' + this._ptyProcess.pid + ' | grep cwd', (error, stdout, stderr) => {
|
||||
if (!error && stdout !== '') {
|
||||
resolve(stdout.substring(stdout.indexOf('/'), stdout.length - 1));
|
||||
} else {
|
||||
this._logService.error('lsof did not run successfully, it may not be on the $PATH?', error, stdout, stderr);
|
||||
resolve(this._initialCwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (platform.isLinux) {
|
||||
return new Promise<string>(resolve => {
|
||||
if (!this._ptyProcess) {
|
||||
resolve(this._initialCwd);
|
||||
return;
|
||||
}
|
||||
this._logService.trace('IPty#pid');
|
||||
fs.readlink('/proc/' + this._ptyProcess.pid + '/cwd', (err, linkedstr) => {
|
||||
if (err) {
|
||||
resolve(this._initialCwd);
|
||||
}
|
||||
resolve(linkedstr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise<string>(resolve => {
|
||||
resolve(this._initialCwd);
|
||||
});
|
||||
}
|
||||
|
||||
public getLatency(): Promise<number> {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the latest resize event to be trigger at a later point.
|
||||
*/
|
||||
class DelayedResizer extends Disposable {
|
||||
public rows: number | undefined;
|
||||
public cols: number | undefined;
|
||||
private _timeout: NodeJS.Timeout;
|
||||
|
||||
private readonly _onTrigger = this._register(new Emitter<{ rows?: number, cols?: number }>());
|
||||
public get onTrigger(): Event<{ rows?: number, cols?: number }> { return this._onTrigger.event; }
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._timeout = setTimeout(() => {
|
||||
this._onTrigger.fire({ rows: this.rows, cols: this.cols });
|
||||
}, 1000);
|
||||
this._register({
|
||||
dispose: () => {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user