mirror of
https://github.com/coder/code-server.git
synced 2026-06-22 09:57:15 +02:00
chore(vscode): update to 1.56.0
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
|
||||
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 { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
|
||||
|
||||
export enum WindowsShellType {
|
||||
@@ -44,6 +44,7 @@ export interface IPtyHostAttachTarget {
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
isOrphan: boolean;
|
||||
icon: string | undefined;
|
||||
}
|
||||
|
||||
export type ITerminalsLayoutInfo = IRawTerminalsLayoutInfo<IPtyHostAttachTarget | null>;
|
||||
@@ -93,9 +94,12 @@ export interface IOffProcessTerminalService {
|
||||
onPtyHostRestart: Event<void>;
|
||||
|
||||
attachToProcess(id: number): Promise<ITerminalChildProcess | undefined>;
|
||||
listProcesses(reduceGraceTime?: boolean): Promise<IProcessDetails[]>;
|
||||
listProcesses(): Promise<IProcessDetails[]>;
|
||||
getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string>;
|
||||
getShellEnvironment(): Promise<IProcessEnvironment>;
|
||||
setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise<void>;
|
||||
getTerminalLayoutInfo(): Promise<ITerminalsLayoutInfo | undefined>;
|
||||
reduceConnectionGraceTime(): Promise<void>;
|
||||
}
|
||||
|
||||
export const ILocalTerminalService = createDecorator<ILocalTerminalService>('localTerminalService');
|
||||
@@ -111,7 +115,6 @@ export interface IPtyService {
|
||||
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 } }>;
|
||||
@@ -142,10 +145,8 @@ export interface IPtyService {
|
||||
|
||||
/**
|
||||
* 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[]>;
|
||||
listProcesses(): Promise<IProcessDetails[]>;
|
||||
|
||||
start(id: number): Promise<ITerminalLaunchError | undefined>;
|
||||
shutdown(id: number, immediate: boolean): Promise<void>;
|
||||
@@ -155,11 +156,15 @@ export interface IPtyService {
|
||||
getCwd(id: number): Promise<string>;
|
||||
getLatency(id: number): Promise<number>;
|
||||
acknowledgeDataEvent(id: number, charCount: number): Promise<void>;
|
||||
processBinary(id: number, data: string): Promise<void>;
|
||||
/** Confirm the process is _not_ an orphan. */
|
||||
orphanQuestionReply(id: number): Promise<void>;
|
||||
|
||||
getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string>;
|
||||
getShellEnvironment(): Promise<IProcessEnvironment>;
|
||||
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void>;
|
||||
getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined>;
|
||||
reduceConnectionGraceTime(): Promise<void>;
|
||||
}
|
||||
|
||||
export enum HeartbeatConstants {
|
||||
@@ -196,6 +201,11 @@ export interface IShellLaunchConfig {
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* An string to follow the name of the terminal with, indicating a special kind of terminal
|
||||
*/
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* The shell executable (bash, cmd, etc.).
|
||||
*/
|
||||
@@ -238,9 +248,9 @@ export interface IShellLaunchConfig {
|
||||
initialText?: string;
|
||||
|
||||
/**
|
||||
* Whether an extension is controlling the terminal via a `vscode.Pseudoterminal`.
|
||||
* Custom PTY/pseudoterminal process to use.
|
||||
*/
|
||||
isExtensionCustomPtyTerminal?: boolean;
|
||||
customPtyImplementation?: (terminalId: number, cols: number, rows: number) => ITerminalChildProcess;
|
||||
|
||||
/**
|
||||
* A UUID generated by the extension host process for terminals created on the extension host process.
|
||||
@@ -250,7 +260,7 @@ export interface IShellLaunchConfig {
|
||||
/**
|
||||
* This is a terminal that attaches to an already running terminal.
|
||||
*/
|
||||
attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; };
|
||||
attachPersistentProcess?: { id: number; pid: number; title: string; cwd: string; icon?: string; };
|
||||
|
||||
/**
|
||||
* Whether the terminal process environment should be exactly as provided in
|
||||
@@ -280,10 +290,25 @@ export interface IShellLaunchConfig {
|
||||
* Whether this terminal was created by an extension.
|
||||
*/
|
||||
isExtensionOwnedTerminal?: boolean;
|
||||
|
||||
/**
|
||||
* The codicon ID to use for this terminal. If not specified it will use the default fallback
|
||||
* icon.
|
||||
*/
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface IShellLaunchConfigDto {
|
||||
name?: string;
|
||||
executable?: string;
|
||||
args?: string[] | string;
|
||||
cwd?: string | UriComponents;
|
||||
env?: ITerminalEnvironment;
|
||||
hideFromUser?: boolean;
|
||||
}
|
||||
|
||||
export interface ITerminalEnvironment {
|
||||
[key: string]: string | null;
|
||||
[key: string]: string | null | undefined;
|
||||
}
|
||||
|
||||
export interface ITerminalLaunchError {
|
||||
@@ -337,6 +362,7 @@ export interface ITerminalChildProcess {
|
||||
*/
|
||||
shutdown(immediate: boolean): void;
|
||||
input(data: string): void;
|
||||
processBinary(data: string): Promise<void>;
|
||||
resize(cols: number, rows: number): void;
|
||||
|
||||
/**
|
||||
@@ -388,7 +414,11 @@ export const enum FlowControlConstants {
|
||||
|
||||
export interface IProcessDataEvent {
|
||||
data: string;
|
||||
sync: boolean;
|
||||
trackCommit: boolean;
|
||||
/**
|
||||
* When trackCommit is set, this will be set to a promise that resolves when the data is parsed.
|
||||
*/
|
||||
writePromise?: Promise<void>;
|
||||
}
|
||||
|
||||
export interface ITerminalDimensions {
|
||||
@@ -409,3 +439,5 @@ export interface ITerminalDimensionsOverride extends Readonly<ITerminalDimension
|
||||
*/
|
||||
forceExactSize?: boolean;
|
||||
}
|
||||
|
||||
export type SafeConfigProvider = <T>(key: string) => T | undefined;
|
||||
|
||||
@@ -7,15 +7,6 @@ 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;
|
||||
@@ -65,6 +56,7 @@ export interface IProcessDetails {
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
isOrphan: boolean;
|
||||
icon: string | undefined;
|
||||
}
|
||||
|
||||
export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo<IProcessDetails>;
|
||||
|
||||
@@ -9,10 +9,11 @@ import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensions
|
||||
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 { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc';
|
||||
import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
enum Constants {
|
||||
MaxRestarts = 5
|
||||
@@ -36,6 +37,7 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
private _proxy: IPtyService;
|
||||
|
||||
private _restartCount = 0;
|
||||
private _isResponsive = true;
|
||||
private _isDisposed = false;
|
||||
|
||||
private _heartbeatFirstTimeout?: NodeJS.Timeout;
|
||||
@@ -49,7 +51,6 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
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 }>());
|
||||
@@ -70,7 +71,8 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -102,6 +104,10 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
|
||||
// Handle exit
|
||||
this._register(client.onDidProcessExit(e => {
|
||||
/* __GDPR__
|
||||
"ptyHost/exit" : {}
|
||||
*/
|
||||
this._telemetryService.publicLog('ptyHost/exit');
|
||||
this._onPtyHostExit.fire(e.code);
|
||||
if (!this._isDisposed) {
|
||||
if (this._restartCount <= Constants.MaxRestarts) {
|
||||
@@ -135,7 +141,7 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
return [client, proxy];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
override dispose() {
|
||||
this._isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
@@ -153,10 +159,12 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
detachFromProcess(id: number): Promise<void> {
|
||||
return this._proxy.detachFromProcess(id);
|
||||
}
|
||||
listProcesses(reduceGraceTime: boolean): Promise<IProcessDetails[]> {
|
||||
return this._proxy.listProcesses(reduceGraceTime);
|
||||
listProcesses(): Promise<IProcessDetails[]> {
|
||||
return this._proxy.listProcesses();
|
||||
}
|
||||
reduceConnectionGraceTime(): Promise<void> {
|
||||
return this._proxy.reduceConnectionGraceTime();
|
||||
}
|
||||
|
||||
start(id: number): Promise<ITerminalLaunchError | undefined> {
|
||||
return this._proxy.start(id);
|
||||
}
|
||||
@@ -166,6 +174,9 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
input(id: number, data: string): Promise<void> {
|
||||
return this._proxy.input(id, data);
|
||||
}
|
||||
processBinary(id: number, data: string): Promise<void> {
|
||||
return this._proxy.processBinary(id, data);
|
||||
}
|
||||
resize(id: number, cols: number, rows: number): Promise<void> {
|
||||
return this._proxy.resize(id, cols, rows);
|
||||
}
|
||||
@@ -185,6 +196,13 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
return this._proxy.orphanQuestionReply(id);
|
||||
}
|
||||
|
||||
getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string> {
|
||||
return this._proxy.getDefaultSystemShell(osOverride);
|
||||
}
|
||||
getShellEnvironment(): Promise<IProcessEnvironment> {
|
||||
return this._proxy.getShellEnvironment();
|
||||
}
|
||||
|
||||
setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
|
||||
return this._proxy.setTerminalLayoutInfo(args);
|
||||
}
|
||||
@@ -193,6 +211,11 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
}
|
||||
|
||||
async restartPtyHost(): Promise<void> {
|
||||
/* __GDPR__
|
||||
"ptyHost/restart" : {}
|
||||
*/
|
||||
this._telemetryService.publicLog('ptyHost/restart');
|
||||
this._isResponsive = true;
|
||||
this._disposePtyHost();
|
||||
[this._client, this._proxy] = this._startPtyHost();
|
||||
}
|
||||
@@ -207,6 +230,13 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
private _handleHeartbeat() {
|
||||
this._clearHeartbeatTimeouts();
|
||||
this._heartbeatFirstTimeout = setTimeout(() => this._handleHeartbeatFirstTimeout(), HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier);
|
||||
if (!this._isResponsive) {
|
||||
/* __GDPR__
|
||||
"ptyHost/responsive" : {}
|
||||
*/
|
||||
this._telemetryService.publicLog('ptyHost/responsive');
|
||||
this._isResponsive = true;
|
||||
}
|
||||
this._onPtyHostResponsive.fire();
|
||||
}
|
||||
|
||||
@@ -219,12 +249,23 @@ export class PtyHostService extends Disposable implements IPtyService {
|
||||
private _handleHeartbeatSecondTimeout() {
|
||||
this._logService.error(`No ptyHost heartbeat after ${(HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier + HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier) / 1000} seconds`);
|
||||
this._heartbeatSecondTimeout = undefined;
|
||||
if (this._isResponsive) {
|
||||
/* __GDPR__
|
||||
"ptyHost/responsive" : {}
|
||||
*/
|
||||
this._telemetryService.publicLog('ptyHost/unresponsive');
|
||||
this._isResponsive = false;
|
||||
}
|
||||
this._onPtyHostUnresponsive.fire();
|
||||
}
|
||||
|
||||
private _handleUnresponsiveCreateProcess() {
|
||||
this._clearHeartbeatTimeouts();
|
||||
this._logService.error(`No ptyHost response to createProcess after ${HeartbeatConstants.CreateProcessTimeout / 1000} seconds`);
|
||||
/* __GDPR__
|
||||
"ptyHost/responsive" : {}
|
||||
*/
|
||||
this._telemetryService.publicLog('ptyHost/responsive');
|
||||
this._onPtyHostUnresponsive.fire();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IProcessEnvironment } from 'vs/base/common/platform';
|
||||
import { IProcessEnvironment, OperatingSystem, OS } from 'vs/base/common/platform';
|
||||
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';
|
||||
@@ -13,6 +13,7 @@ import { TerminalProcess } from 'vs/platform/terminal/node/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';
|
||||
import { getSystemShell } from 'vs/base/node/shell';
|
||||
|
||||
type WorkspaceId = string;
|
||||
|
||||
@@ -90,7 +91,7 @@ export class PtyService extends Disposable implements IPtyService {
|
||||
if (process.onProcessResolvedShellLaunchConfig) {
|
||||
process.onProcessResolvedShellLaunchConfig(event => this._onProcessResolvedShellLaunchConfig.fire({ id, event }));
|
||||
}
|
||||
const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService);
|
||||
const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, this._logService, shellLaunchConfig.icon);
|
||||
process.onProcessExit(() => {
|
||||
persistentProcess.dispose();
|
||||
this._ptys.delete(id);
|
||||
@@ -117,13 +118,13 @@ 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();
|
||||
}
|
||||
async reduceConnectionGraceTime(): Promise<void> {
|
||||
for (const pty of this._ptys.values()) {
|
||||
pty.reduceGraceTime();
|
||||
}
|
||||
}
|
||||
|
||||
async listProcesses(): Promise<IProcessDetails[]> {
|
||||
const persistentProcesses = Array.from(this._ptys.entries()).filter(([_, pty]) => pty.shouldPersistTerminal);
|
||||
|
||||
this._logService.info(`Listing ${persistentProcesses.length} persistent terminals, ${this._ptys.size} total terminals`);
|
||||
@@ -136,11 +137,15 @@ export class PtyService extends Disposable implements IPtyService {
|
||||
return this._throwIfNoPty(id).start();
|
||||
}
|
||||
async shutdown(id: number, immediate: boolean): Promise<void> {
|
||||
return this._throwIfNoPty(id).shutdown(immediate);
|
||||
// Don't throw if the pty is already shutdown
|
||||
return this._ptys.get(id)?.shutdown(immediate);
|
||||
}
|
||||
async input(id: number, data: string): Promise<void> {
|
||||
return this._throwIfNoPty(id).input(data);
|
||||
}
|
||||
async processBinary(id: number, data: string): Promise<void> {
|
||||
return this._throwIfNoPty(id).writeBinary(data);
|
||||
}
|
||||
async resize(id: number, cols: number, rows: number): Promise<void> {
|
||||
return this._throwIfNoPty(id).resize(cols, rows);
|
||||
}
|
||||
@@ -160,6 +165,14 @@ export class PtyService extends Disposable implements IPtyService {
|
||||
return this._throwIfNoPty(id).orphanQuestionReply();
|
||||
}
|
||||
|
||||
async getDefaultSystemShell(osOverride: OperatingSystem = OS): Promise<string> {
|
||||
return getSystemShell(osOverride, process.env);
|
||||
}
|
||||
|
||||
async getShellEnvironment(): Promise<IProcessEnvironment> {
|
||||
return { ...process.env };
|
||||
}
|
||||
|
||||
async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
|
||||
this._workspaceLayoutInfos.set(args.workspaceId, args);
|
||||
}
|
||||
@@ -213,7 +226,8 @@ export class PtyService extends Disposable implements IPtyService {
|
||||
workspaceId: persistentProcess.workspaceId,
|
||||
workspaceName: persistentProcess.workspaceName,
|
||||
cwd,
|
||||
isOrphan
|
||||
isOrphan,
|
||||
icon: persistentProcess.icon
|
||||
};
|
||||
}
|
||||
|
||||
@@ -251,7 +265,7 @@ export class PersistentTerminalProcess extends Disposable {
|
||||
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>());
|
||||
private readonly _onProcessData = this._register(new Emitter<string>());
|
||||
readonly onProcessData = this._onProcessData.event;
|
||||
private readonly _onProcessOrphanQuestion = this._register(new Emitter<void>());
|
||||
readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
|
||||
@@ -263,6 +277,7 @@ export class PersistentTerminalProcess extends Disposable {
|
||||
|
||||
get pid(): number { return this._pid; }
|
||||
get title(): string { return this._terminalProcess.currentTitle; }
|
||||
get icon(): string | undefined { return this._icon; }
|
||||
|
||||
constructor(
|
||||
private _persistentProcessId: number,
|
||||
@@ -271,7 +286,8 @@ export class PersistentTerminalProcess extends Disposable {
|
||||
public readonly workspaceName: string,
|
||||
public readonly shouldPersistTerminal: boolean,
|
||||
cols: number, rows: number,
|
||||
private readonly _logService: ILogService
|
||||
private readonly _logService: ILogService,
|
||||
private readonly _icon?: string
|
||||
) {
|
||||
super();
|
||||
this._recorder = new TerminalRecorder(cols, rows);
|
||||
@@ -295,12 +311,12 @@ export class PersistentTerminalProcess extends Disposable {
|
||||
this._register(this._terminalProcess.onProcessShellTypeChanged(e => this._onProcessShellTypeChanged.fire(e)));
|
||||
|
||||
// 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._bufferer = new TerminalDataBufferer((_, data) => this._onProcessData.fire(data));
|
||||
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)));
|
||||
this._register(this.onProcessData(e => this._recorder.recordData(e)));
|
||||
}
|
||||
|
||||
attach(): void {
|
||||
@@ -340,6 +356,9 @@ export class PersistentTerminalProcess extends Disposable {
|
||||
}
|
||||
return this._terminalProcess.input(data);
|
||||
}
|
||||
writeBinary(data: string): Promise<void> {
|
||||
return this._terminalProcess.processBinary(data);
|
||||
}
|
||||
resize(cols: number, rows: number): void {
|
||||
if (this._inReplay) {
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,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 * as pfs 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';
|
||||
@@ -20,7 +20,7 @@ export function getWindowsBuildNumber(): number {
|
||||
return buildNumber;
|
||||
}
|
||||
|
||||
export async function findExecutable(command: string, cwd?: string, paths?: string[], env: IProcessEnvironment = process.env as IProcessEnvironment): Promise<string | undefined> {
|
||||
export async function findExecutable(command: string, cwd?: string, paths?: string[], env: IProcessEnvironment = process.env as IProcessEnvironment, exists: (path: string) => Promise<boolean> = pfs.exists): Promise<string | undefined> {
|
||||
// If we have an absolute path then we take it.
|
||||
if (path.isAbsolute(command)) {
|
||||
return await exists(command) ? command : undefined;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
@@ -17,6 +16,8 @@ import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper';
|
||||
import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
// 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
|
||||
@@ -44,9 +45,33 @@ const enum ShutdownConstants {
|
||||
MaximumShutdownTime = 5000
|
||||
}
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* The minimum duration between kill and spawn calls on Windows/conpty as a mitigation for a
|
||||
* hang issue. See:
|
||||
* - https://github.com/microsoft/vscode/issues/71966
|
||||
* - https://github.com/microsoft/vscode/issues/117956
|
||||
* - https://github.com/microsoft/vscode/issues/121336
|
||||
*/
|
||||
KillSpawnThrottleInterval = 250,
|
||||
/**
|
||||
* The amount of time to wait when a call is throttles beyond the exact amount, this is used to
|
||||
* try prevent early timeouts causing a kill/spawn call to happen at double the regular
|
||||
* interval.
|
||||
*/
|
||||
KillSpawnSpacingDuration = 50,
|
||||
}
|
||||
|
||||
interface IWriteObject {
|
||||
data: string,
|
||||
isBinary: boolean
|
||||
}
|
||||
|
||||
export class TerminalProcess extends Disposable implements ITerminalChildProcess {
|
||||
readonly shouldPersist = false;
|
||||
|
||||
private static _lastKillOrStart = 0;
|
||||
|
||||
private _exitCode: number | undefined;
|
||||
private _exitMessage: string | undefined;
|
||||
private _closeTimeout: any;
|
||||
@@ -62,7 +87,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
private _isDisposed: boolean = false;
|
||||
private _windowsShellHelper: WindowsShellHelper | undefined;
|
||||
private _titleInterval: NodeJS.Timer | null = null;
|
||||
private _writeQueue: string[] = [];
|
||||
private _writeQueue: IWriteObject[] = [];
|
||||
private _writeTimeout: NodeJS.Timeout | undefined;
|
||||
private _delayedResizer: DelayedResizer | undefined;
|
||||
private readonly _initialCwd: string;
|
||||
@@ -93,17 +118,17 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
cwd: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
env: platform.IProcessEnvironment,
|
||||
env: IProcessEnvironment,
|
||||
/**
|
||||
* environment used for `findExecutable`
|
||||
*/
|
||||
private readonly _executableEnv: platform.IProcessEnvironment,
|
||||
private readonly _executableEnv: IProcessEnvironment,
|
||||
windowsEnableConpty: boolean,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
let name: string;
|
||||
if (platform.isWindows) {
|
||||
if (isWindows) {
|
||||
name = path.basename(this._shellLaunchConfig.executable || '');
|
||||
} else {
|
||||
// Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a
|
||||
@@ -115,7 +140,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
this._ptyOptions = {
|
||||
name,
|
||||
cwd,
|
||||
env,
|
||||
// TODO: When node-pty is updated this cast can be removed
|
||||
env: env as { [key: string]: string; },
|
||||
cols,
|
||||
rows,
|
||||
useConpty,
|
||||
@@ -123,7 +149,7 @@ 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) {
|
||||
if (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 => {
|
||||
@@ -194,6 +220,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
if (!executable) {
|
||||
return { message: localize('launchFail.executableDoesNotExist', "Path to shell executable \"{0}\" does not exist", slc.executable) };
|
||||
}
|
||||
// Set the executable explicitly here so that node-pty doesn't need to search the
|
||||
// $PATH too.
|
||||
slc.executable = executable;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -201,6 +230,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
|
||||
private async setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): Promise<void> {
|
||||
const args = shellLaunchConfig.args || [];
|
||||
await this._throttleKillSpawn();
|
||||
this._logService.trace('IPty#spawn', shellLaunchConfig.executable, args, options);
|
||||
const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options);
|
||||
this._ptyProcess = ptyProcess;
|
||||
@@ -220,7 +250,6 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
// Refire the data event
|
||||
this._onProcessData.fire(data);
|
||||
if (this._closeTimeout) {
|
||||
clearTimeout(this._closeTimeout);
|
||||
this._queueProcessExit();
|
||||
}
|
||||
this._windowsShellHelper?.checkShell();
|
||||
@@ -233,7 +262,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
this._sendProcessId(ptyProcess.pid);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
public override dispose(): void {
|
||||
this._isDisposed = true;
|
||||
if (this._titleInterval) {
|
||||
clearInterval(this._titleInterval);
|
||||
@@ -246,7 +275,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
// 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) {
|
||||
if (!isWindows) {
|
||||
this._titleInterval = setInterval(() => {
|
||||
if (this._currentTitle !== ptyProcess.process) {
|
||||
this._sendProcessTitle(ptyProcess);
|
||||
@@ -278,6 +307,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
// point but we want to make sure
|
||||
try {
|
||||
if (this._ptyProcess) {
|
||||
await this._throttleKillSpawn();
|
||||
this._logService.trace('IPty#kill');
|
||||
this._ptyProcess.kill();
|
||||
}
|
||||
@@ -288,6 +318,19 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private async _throttleKillSpawn(): Promise<void> {
|
||||
// Only throttle on Windows/conpty
|
||||
if (!isWindows || !('useConpty' in this._ptyOptions) || !this._ptyOptions.useConpty) {
|
||||
return;
|
||||
}
|
||||
// Use a loop to ensure multiple calls in a single interval space out
|
||||
while (Date.now() - TerminalProcess._lastKillOrStart < Constants.KillSpawnThrottleInterval) {
|
||||
this._logService.trace('Throttling kill/spawn call');
|
||||
await timeout(Constants.KillSpawnThrottleInterval - (Date.now() - TerminalProcess._lastKillOrStart) + Constants.KillSpawnSpacingDuration);
|
||||
}
|
||||
TerminalProcess._lastKillOrStart = Date.now();
|
||||
}
|
||||
|
||||
private _sendProcessId(pid: number) {
|
||||
this._onProcessReady.fire({ pid, cwd: this._initialCwd });
|
||||
}
|
||||
@@ -301,7 +344,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
}
|
||||
|
||||
public shutdown(immediate: boolean): void {
|
||||
if (immediate) {
|
||||
// don't force immediate disposal of the terminal processes on Windows as an additional
|
||||
// mitigation for https://github.com/microsoft/vscode/issues/71966 which causes the pty host
|
||||
// to become unresponsive, disconnecting all terminals across all windows.
|
||||
if (immediate && !isWindows) {
|
||||
this._kill();
|
||||
} else {
|
||||
if (!this._closeTimeout && !this._isDisposed) {
|
||||
@@ -317,16 +363,24 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
}
|
||||
}
|
||||
|
||||
public input(data: string): void {
|
||||
public input(data: string, isBinary?: boolean): 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));
|
||||
const obj = {
|
||||
isBinary: isBinary || false,
|
||||
data: data.substr(i * WRITE_MAX_CHUNK_SIZE, WRITE_MAX_CHUNK_SIZE)
|
||||
};
|
||||
this._writeQueue.push(obj);
|
||||
}
|
||||
this._startWrite();
|
||||
}
|
||||
|
||||
public async processBinary(data: string): Promise<void> {
|
||||
this.input(data, true);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -349,9 +403,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
}
|
||||
|
||||
private _doWrite(): void {
|
||||
const data = this._writeQueue.shift()!;
|
||||
this._logService.trace('IPty#write', `${data.length} characters`);
|
||||
this._ptyProcess!.write(data);
|
||||
const object = this._writeQueue.shift()!;
|
||||
if (object.isBinary) {
|
||||
this._ptyProcess!.write(Buffer.from(object.data, 'binary') as any);
|
||||
} else {
|
||||
this._ptyProcess!.write(object.data);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
@@ -412,7 +469,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
}
|
||||
|
||||
public getCwd(): Promise<string> {
|
||||
if (platform.isMacintosh) {
|
||||
if (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('.');
|
||||
@@ -435,7 +492,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
|
||||
}
|
||||
}
|
||||
|
||||
if (platform.isLinux) {
|
||||
if (isLinux) {
|
||||
return new Promise<string>(resolve => {
|
||||
if (!this._ptyProcess) {
|
||||
resolve(this._initialCwd);
|
||||
@@ -484,7 +541,7 @@ class DelayedResizer extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* 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';
|
||||
import { isWindows, platform } from 'vs/base/common/platform';
|
||||
|
||||
export interface IWindowsShellHelper extends IDisposable {
|
||||
readonly onShellNameChanged: Event<string>;
|
||||
@@ -51,8 +51,8 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe
|
||||
) {
|
||||
super();
|
||||
|
||||
if (!platform.isWindows) {
|
||||
throw new Error(`WindowsShellHelper cannot be instantiated on ${platform.platform}`);
|
||||
if (!isWindows) {
|
||||
throw new Error(`WindowsShellHelper cannot be instantiated on ${platform}`);
|
||||
}
|
||||
|
||||
this._isDisposed = false;
|
||||
@@ -69,7 +69,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe
|
||||
|
||||
@debounce(500)
|
||||
async checkShell(): Promise<void> {
|
||||
if (platform.isWindows) {
|
||||
if (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
|
||||
@@ -112,7 +112,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe
|
||||
return this.traverseTree(tree.children[favouriteChild]);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
public override dispose(): void {
|
||||
this._isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user