@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
1,016 lines (1,015 loc) • 33.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { execFile, exec } from 'child_process';
import { AutoOpenBarrier, ProcessTimeRunOnceScheduler, Promises, Queue } from '@sussudio/base/common/async.mjs';
import { Emitter } from '@sussudio/base/common/event.mjs';
import { Disposable, toDisposable } from '@sussudio/base/common/lifecycle.mjs';
import { isWindows, OS } from '@sussudio/base/common/platform.mjs';
import { getSystemShell } from '@sussudio/base/node/shell.mjs';
import { RequestStore } from '../common/requestStore.mjs';
import { TitleEventSource } from '../common/terminal.mjs';
import { TerminalDataBufferer } from '../common/terminalDataBuffering.mjs';
import { escapeNonWindowsPath } from '../common/terminalEnvironment.mjs';
import { Terminal as XtermTerminal } from 'xterm-headless';
import { getWindowsBuildNumber } from './terminalEnvironment.mjs';
import { TerminalProcess } from './terminalProcess.mjs';
import { localize } from 'vscode-nls.mjs';
import { ignoreProcessNames } from './childProcessMonitor.mjs';
import { TerminalAutoResponder } from '../common/terminalAutoResponder.mjs';
import { ErrorNoTelemetry } from '@sussudio/base/common/errors.mjs';
import { ShellIntegrationAddon } from '../common/xterm/shellIntegrationAddon.mjs';
import { formatMessageForTerminal } from '../common/terminalStrings.mjs';
import { join } from 'path';
let SerializeAddon;
let Unicode11Addon;
export class PtyService extends Disposable {
_lastPtyId;
_logService;
_productService;
_reconnectConstants;
_ptys = new Map();
_workspaceLayoutInfos = new Map();
_detachInstanceRequestStore;
_revivedPtyIdMap = new Map();
_autoReplies = new Map();
_onHeartbeat = this._register(new Emitter());
onHeartbeat = this._onHeartbeat.event;
_onProcessData = this._register(new Emitter());
onProcessData = this._onProcessData.event;
_onProcessReplay = this._register(new Emitter());
onProcessReplay = this._onProcessReplay.event;
_onProcessReady = this._register(new Emitter());
onProcessReady = this._onProcessReady.event;
_onProcessExit = this._register(new Emitter());
onProcessExit = this._onProcessExit.event;
_onProcessOrphanQuestion = this._register(new Emitter());
onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
_onDidRequestDetach = this._register(new Emitter());
onDidRequestDetach = this._onDidRequestDetach.event;
_onDidChangeProperty = this._register(new Emitter());
onDidChangeProperty = this._onDidChangeProperty.event;
constructor(_lastPtyId, _logService, _productService, _reconnectConstants) {
super();
this._lastPtyId = _lastPtyId;
this._logService = _logService;
this._productService = _productService;
this._reconnectConstants = _reconnectConstants;
this._register(
toDisposable(() => {
for (const pty of this._ptys.values()) {
pty.shutdown(true);
}
this._ptys.clear();
}),
);
this._detachInstanceRequestStore = this._register(new RequestStore(undefined, this._logService));
this._detachInstanceRequestStore.onCreateRequest(this._onDidRequestDetach.fire, this._onDidRequestDetach);
}
async refreshIgnoreProcessNames(names) {
ignoreProcessNames.length = 0;
ignoreProcessNames.push(...names);
}
onPtyHostExit;
onPtyHostStart;
onPtyHostUnresponsive;
onPtyHostResponsive;
onPtyHostRequestResolveVariables;
async requestDetachInstance(workspaceId, instanceId) {
return this._detachInstanceRequestStore.createRequest({ workspaceId, instanceId });
}
async acceptDetachInstanceReply(requestId, persistentProcessId) {
let processDetails = undefined;
const pty = this._ptys.get(persistentProcessId);
if (pty) {
processDetails = await this._buildProcessDetails(persistentProcessId, pty);
}
this._detachInstanceRequestStore.acceptReply(requestId, processDetails);
}
async freePortKillProcess(port) {
const stdout = await new Promise((resolve, reject) => {
exec(
isWindows ? `netstat -ano | findstr "${port}"` : `lsof -nP -iTCP -sTCP:LISTEN | grep ${port}`,
{},
(err, stdout) => {
if (err) {
return reject('Problem occurred when listing active processes');
}
resolve(stdout);
},
);
});
const processesForPort = stdout.split('\n');
if (processesForPort.length >= 1) {
const capturePid = /\s+(\d+)\s+/;
const processId = processesForPort[0].match(capturePid)?.[1];
if (processId) {
try {
process.kill(Number.parseInt(processId));
} catch {}
} else {
throw new Error(`Processes for port ${port} were not found`);
}
return { port, processId };
}
throw new Error(`Could not kill process with port ${port}`);
}
async serializeTerminalState(ids) {
const promises = [];
for (const [persistentProcessId, persistentProcess] of this._ptys.entries()) {
// Only serialize persistent processes that have had data written or performed a replay
if (persistentProcess.hasWrittenData && ids.indexOf(persistentProcessId) !== -1) {
promises.push(
Promises.withAsyncBody(async (r) => {
r({
id: persistentProcessId,
shellLaunchConfig: persistentProcess.shellLaunchConfig,
processDetails: await this._buildProcessDetails(persistentProcessId, persistentProcess),
processLaunchConfig: persistentProcess.processLaunchOptions,
unicodeVersion: persistentProcess.unicodeVersion,
replayEvent: await persistentProcess.serializeNormalBuffer(),
timestamp: Date.now(),
});
}),
);
}
}
const serialized = {
version: 1,
state: await Promise.all(promises),
};
return JSON.stringify(serialized);
}
async reviveTerminalProcesses(state, dateTimeFormatLocale) {
for (const terminal of state) {
const restoreMessage = localize('terminal-history-restored', 'History restored');
// TODO: We may at some point want to show date information in a hover via a custom sequence:
// new Date(terminal.timestamp).toLocaleDateString(dateTimeFormatLocale)
// new Date(terminal.timestamp).toLocaleTimeString(dateTimeFormatLocale)
const newId = await this.createProcess(
{
...terminal.shellLaunchConfig,
cwd: terminal.processDetails.cwd,
color: terminal.processDetails.color,
icon: terminal.processDetails.icon,
name:
terminal.processDetails.titleSource === TitleEventSource.Api ? terminal.processDetails.title : undefined,
initialText:
terminal.replayEvent.events[0].data + formatMessageForTerminal(restoreMessage, { loudFormatting: true }),
},
terminal.processDetails.cwd,
terminal.replayEvent.events[0].cols,
terminal.replayEvent.events[0].rows,
terminal.unicodeVersion,
terminal.processLaunchConfig.env,
terminal.processLaunchConfig.executableEnv,
terminal.processLaunchConfig.options,
true,
terminal.processDetails.workspaceId,
terminal.processDetails.workspaceName,
true,
terminal.replayEvent.events[0].data,
);
// Don't start the process here as there's no terminal to answer CPR
this._revivedPtyIdMap.set(terminal.id, { newId, state: terminal });
}
}
async shutdownAll() {
this.dispose();
}
async createProcess(
shellLaunchConfig,
cwd,
cols,
rows,
unicodeVersion,
env,
executableEnv,
options,
shouldPersist,
workspaceId,
workspaceName,
isReviving,
rawReviveBuffer,
) {
if (shellLaunchConfig.attachPersistentProcess) {
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,
options,
this._logService,
this._productService,
);
process.onProcessData((event) => this._onProcessData.fire({ id, event }));
const processLaunchOptions = {
env,
executableEnv,
options,
};
const persistentProcess = new PersistentTerminalProcess(
id,
process,
workspaceId,
workspaceName,
shouldPersist,
cols,
rows,
processLaunchOptions,
unicodeVersion,
this._reconnectConstants,
this._logService,
isReviving && typeof shellLaunchConfig.initialText === 'string' ? shellLaunchConfig.initialText : undefined,
rawReviveBuffer,
shellLaunchConfig.icon,
shellLaunchConfig.color,
shellLaunchConfig.name,
shellLaunchConfig.fixedDimensions,
);
process.onDidChangeProperty((property) => this._onDidChangeProperty.fire({ id, property }));
process.onProcessExit((event) => {
persistentProcess.dispose();
this._ptys.delete(id);
this._onProcessExit.fire({ id, event });
});
persistentProcess.onProcessReplay((event) => this._onProcessReplay.fire({ id, event }));
persistentProcess.onProcessReady((event) => this._onProcessReady.fire({ id, event }));
persistentProcess.onProcessOrphanQuestion(() => this._onProcessOrphanQuestion.fire({ id }));
persistentProcess.onDidChangeProperty((property) => this._onDidChangeProperty.fire({ id, property }));
persistentProcess.onPersistentProcessReady(() => {
for (const e of this._autoReplies.entries()) {
persistentProcess.installAutoReply(e[0], e[1]);
}
});
this._ptys.set(id, persistentProcess);
return id;
}
async attachToProcess(id) {
try {
await this._throwIfNoPty(id).attach();
this._logService.info(`Persistent process reconnection "${id}"`);
} catch (e) {
this._logService.warn(`Persistent process reconnection "${id}" failed`, e.message);
throw e;
}
}
async updateTitle(id, title, titleSource) {
this._throwIfNoPty(id).setTitle(title, titleSource);
}
async updateIcon(id, userInitiated, icon, color) {
this._throwIfNoPty(id).setIcon(userInitiated, icon, color);
}
async refreshProperty(id, type) {
return this._throwIfNoPty(id).refreshProperty(type);
}
async updateProperty(id, type, value) {
return this._throwIfNoPty(id).updateProperty(type, value);
}
async detachFromProcess(id, forcePersist) {
return this._throwIfNoPty(id).detach(forcePersist);
}
async reduceConnectionGraceTime() {
for (const pty of this._ptys.values()) {
pty.reduceGraceTime();
}
}
async listProcesses() {
const persistentProcesses = Array.from(this._ptys.entries()).filter(([_, pty]) => pty.shouldPersistTerminal);
this._logService.info(
`Listing ${persistentProcesses.length} persistent terminals, ${this._ptys.size} total terminals`,
);
const promises = persistentProcesses.map(async ([id, terminalProcessData]) =>
this._buildProcessDetails(id, terminalProcessData),
);
const allTerminals = await Promise.all(promises);
return allTerminals.filter((entry) => entry.isOrphan);
}
async start(id) {
this._logService.trace('ptyService#start', id);
const pty = this._ptys.get(id);
return pty ? pty.start() : { message: `Could not find pty with id "${id}"` };
}
async shutdown(id, immediate) {
// Don't throw if the pty is already shutdown
this._logService.trace('ptyService#shutDown', id, immediate);
return this._ptys.get(id)?.shutdown(immediate);
}
async input(id, data) {
return this._throwIfNoPty(id).input(data);
}
async processBinary(id, data) {
return this._throwIfNoPty(id).writeBinary(data);
}
async resize(id, cols, rows) {
return this._throwIfNoPty(id).resize(cols, rows);
}
async getInitialCwd(id) {
return this._throwIfNoPty(id).getInitialCwd();
}
async getCwd(id) {
return this._throwIfNoPty(id).getCwd();
}
async acknowledgeDataEvent(id, charCount) {
return this._throwIfNoPty(id).acknowledgeDataEvent(charCount);
}
async setUnicodeVersion(id, version) {
return this._throwIfNoPty(id).setUnicodeVersion(version);
}
async getLatency(id) {
return 0;
}
async orphanQuestionReply(id) {
return this._throwIfNoPty(id).orphanQuestionReply();
}
async installAutoReply(match, reply) {
this._autoReplies.set(match, reply);
// If the auto reply exists on any existing terminals it will be overridden
for (const p of this._ptys.values()) {
p.installAutoReply(match, reply);
}
}
async uninstallAllAutoReplies() {
for (const match of this._autoReplies.keys()) {
for (const p of this._ptys.values()) {
p.uninstallAutoReply(match);
}
}
}
async uninstallAutoReply(match) {
for (const p of this._ptys.values()) {
p.uninstallAutoReply(match);
}
}
async getDefaultSystemShell(osOverride = OS) {
return getSystemShell(osOverride, process.env);
}
async getEnvironment() {
return { ...process.env };
}
async getWslPath(original, direction) {
if (direction === 'win-to-unix') {
if (!isWindows) {
return original;
}
if (getWindowsBuildNumber() < 17063) {
return original.replace(/\\/g, '/');
}
const wslExecutable = this._getWSLExecutablePath();
if (!wslExecutable) {
return original;
}
return new Promise((c) => {
const proc = execFile(wslExecutable, ['-e', 'wslpath', original], {}, (error, stdout, stderr) => {
c(escapeNonWindowsPath(stdout.trim()));
});
proc.stdin.end();
});
}
if (direction === 'unix-to-win') {
// The backend is Windows, for example a local Windows workspace with a wsl session in
// the terminal.
if (isWindows) {
if (getWindowsBuildNumber() < 17063) {
return original;
}
const wslExecutable = this._getWSLExecutablePath();
if (!wslExecutable) {
return original;
}
return new Promise((c) => {
const proc = execFile(wslExecutable, ['-e', 'wslpath', '-w', original], {}, (error, stdout, stderr) => {
c(stdout.trim());
});
proc.stdin.end();
});
}
}
// Fallback just in case
return original;
}
_getWSLExecutablePath() {
const useWSLexe = getWindowsBuildNumber() >= 16299;
const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
const systemRoot = process.env['SystemRoot'];
if (systemRoot) {
return join(systemRoot, is32ProcessOn64Windows ? 'Sysnative' : 'System32', useWSLexe ? 'wsl.exe' : 'bash.exe');
}
return undefined;
}
async getRevivedPtyNewId(id) {
try {
return this._revivedPtyIdMap.get(id)?.newId;
} catch (e) {
this._logService.warn(`Couldn't find terminal ID ${id}`, e.message);
}
return undefined;
}
async setTerminalLayoutInfo(args) {
this._logService.trace('ptyService#setLayoutInfo', args.tabs);
this._workspaceLayoutInfos.set(args.workspaceId, args);
}
async getTerminalLayoutInfo(args) {
const layout = this._workspaceLayoutInfos.get(args.workspaceId);
this._logService.trace('ptyService#getLayoutInfo', args);
if (layout) {
const expandedTabs = await Promise.all(layout.tabs.map(async (tab) => this._expandTerminalTab(tab)));
const tabs = expandedTabs.filter((t) => t.terminals.length > 0);
this._logService.trace('ptyService#returnLayoutInfo', tabs);
return { tabs };
}
return undefined;
}
async _expandTerminalTab(tab) {
const expandedTerminals = await Promise.all(tab.terminals.map((t) => this._expandTerminalInstance(t)));
const filtered = expandedTerminals.filter((term) => term.terminal !== null);
return {
isActive: tab.isActive,
activePersistentProcessId: tab.activePersistentProcessId,
terminals: filtered,
};
}
async _expandTerminalInstance(t) {
try {
const revivedPtyId = this._revivedPtyIdMap.get(t.terminal)?.newId;
this._revivedPtyIdMap.delete(t.terminal);
const persistentProcessId = revivedPtyId ?? t.terminal;
const persistentProcess = this._throwIfNoPty(persistentProcessId);
const processDetails =
persistentProcess &&
(await this._buildProcessDetails(t.terminal, persistentProcess, revivedPtyId !== undefined));
return {
terminal: { ...processDetails, id: persistentProcessId },
relativeSize: t.relativeSize,
};
} catch (e) {
this._logService.warn(`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,
};
}
}
async _buildProcessDetails(id, persistentProcess, wasRevived = false) {
// If the process was just revived, don't do the orphan check as it will
// take some time
const [cwd, isOrphan] = await Promise.all([
persistentProcess.getCwd(),
wasRevived ? true : persistentProcess.isOrphaned(),
]);
return {
id,
title: persistentProcess.title,
titleSource: persistentProcess.titleSource,
pid: persistentProcess.pid,
workspaceId: persistentProcess.workspaceId,
workspaceName: persistentProcess.workspaceName,
cwd,
isOrphan,
icon: persistentProcess.icon,
color: persistentProcess.color,
fixedDimensions: persistentProcess.fixedDimensions,
environmentVariableCollections: persistentProcess.processLaunchOptions.options.environmentVariableCollections,
reconnectionProperties: persistentProcess.shellLaunchConfig.reconnectionProperties,
waitOnExit: persistentProcess.shellLaunchConfig.waitOnExit,
hideFromUser: persistentProcess.shellLaunchConfig.hideFromUser,
isFeatureTerminal: persistentProcess.shellLaunchConfig.isFeatureTerminal,
type: persistentProcess.shellLaunchConfig.type,
hasChildProcesses: persistentProcess.hasChildProcesses,
};
}
_throwIfNoPty(id) {
const pty = this._ptys.get(id);
if (!pty) {
throw new ErrorNoTelemetry(`Could not find pty on pty host`);
}
return pty;
}
}
class PersistentTerminalProcess extends Disposable {
_persistentProcessId;
_terminalProcess;
workspaceId;
workspaceName;
shouldPersistTerminal;
processLaunchOptions;
unicodeVersion;
_logService;
_icon;
_color;
_bufferer;
_autoReplies = new Map();
_pendingCommands = new Map();
_isStarted = false;
_interactionState;
_orphanQuestionBarrier;
_orphanQuestionReplyTime;
_orphanRequestQueue = new Queue();
_disconnectRunner1;
_disconnectRunner2;
_onProcessReplay = this._register(new Emitter());
onProcessReplay = this._onProcessReplay.event;
_onProcessReady = this._register(new Emitter());
onProcessReady = this._onProcessReady.event;
_onPersistentProcessReady = this._register(new Emitter());
/** Fired when the persistent process has a ready process and has finished its replay. */
onPersistentProcessReady = this._onPersistentProcessReady.event;
_onProcessData = this._register(new Emitter());
onProcessData = this._onProcessData.event;
_onProcessOrphanQuestion = this._register(new Emitter());
onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
_onDidChangeProperty = this._register(new Emitter());
onDidChangeProperty = this._onDidChangeProperty.event;
_inReplay = false;
_pid = -1;
_cwd = '';
_title;
_titleSource = TitleEventSource.Process;
_serializer;
_wasRevived;
_fixedDimensions;
get pid() {
return this._pid;
}
get shellLaunchConfig() {
return this._terminalProcess.shellLaunchConfig;
}
get hasWrittenData() {
return this._interactionState.value !== 'None' /* InteractionState.None */;
}
get title() {
return this._title || this._terminalProcess.currentTitle;
}
get titleSource() {
return this._titleSource;
}
get icon() {
return this._icon;
}
get color() {
return this._color;
}
get fixedDimensions() {
return this._fixedDimensions;
}
get hasChildProcesses() {
return this._terminalProcess.hasChildProcesses;
}
setTitle(title, titleSource) {
if (titleSource === TitleEventSource.Api) {
this._interactionState.setValue('Session' /* InteractionState.Session */, 'setTitle');
this._serializer.freeRawReviveBuffer();
}
this._title = title;
this._titleSource = titleSource;
}
setIcon(userInitiated, icon, color) {
if (
!this._icon ||
('id' in icon && 'id' in this._icon && icon.id !== this._icon.id) ||
!this.color ||
color !== this._color
) {
this._serializer.freeRawReviveBuffer();
if (userInitiated) {
this._interactionState.setValue('Session' /* InteractionState.Session */, 'setIcon');
}
}
this._icon = icon;
this._color = color;
}
_setFixedDimensions(fixedDimensions) {
this._fixedDimensions = fixedDimensions;
}
constructor(
_persistentProcessId,
_terminalProcess,
workspaceId,
workspaceName,
shouldPersistTerminal,
cols,
rows,
processLaunchOptions,
unicodeVersion,
reconnectConstants,
_logService,
reviveBuffer,
rawReviveBuffer,
_icon,
_color,
name,
fixedDimensions,
) {
super();
this._persistentProcessId = _persistentProcessId;
this._terminalProcess = _terminalProcess;
this.workspaceId = workspaceId;
this.workspaceName = workspaceName;
this.shouldPersistTerminal = shouldPersistTerminal;
this.processLaunchOptions = processLaunchOptions;
this.unicodeVersion = unicodeVersion;
this._logService = _logService;
this._icon = _icon;
this._color = _color;
this._logService.trace('persistentTerminalProcess#ctor', _persistentProcessId, arguments);
this._interactionState = new MutationLogger(
`Persistent process "${this._persistentProcessId}" interaction state`,
'None' /* InteractionState.None */,
this._logService,
);
this._wasRevived = reviveBuffer !== undefined;
this._serializer = new XtermSerializer(
cols,
rows,
reconnectConstants.scrollback,
unicodeVersion,
reviveBuffer,
shouldPersistTerminal ? rawReviveBuffer : undefined,
this._logService,
);
if (name) {
this.setTitle(name, TitleEventSource.Api);
}
this._fixedDimensions = fixedDimensions;
this._orphanQuestionBarrier = null;
this._orphanQuestionReplyTime = 0;
this._disconnectRunner1 = this._register(
new ProcessTimeRunOnceScheduler(() => {
this._logService.info(
`Persistent process "${this._persistentProcessId}": The reconnection grace time of ${printTime(
reconnectConstants.graceTime,
)} has expired, shutting down pid "${this._pid}"`,
);
this.shutdown(true);
}, reconnectConstants.graceTime),
);
this._disconnectRunner2 = this._register(
new ProcessTimeRunOnceScheduler(() => {
this._logService.info(
`Persistent process "${this._persistentProcessId}": The short reconnection grace time of ${printTime(
reconnectConstants.shortGraceTime,
)} has expired, shutting down pid ${this._pid}`,
);
this.shutdown(true);
}, reconnectConstants.shortGraceTime),
);
this._register(this._terminalProcess.onProcessExit(() => this._bufferer.stopBuffering(this._persistentProcessId)));
this._register(
this._terminalProcess.onProcessReady((e) => {
this._pid = e.pid;
this._cwd = e.cwd;
this._onProcessReady.fire(e);
}),
);
this._register(
this._terminalProcess.onDidChangeProperty((e) => {
this._onDidChangeProperty.fire(e);
}),
);
// Data buffering to reduce the amount of messages going to the renderer
this._bufferer = new TerminalDataBufferer((_, data) => this._onProcessData.fire(data));
this._register(this._bufferer.startBuffering(this._persistentProcessId, this._terminalProcess.onProcessData));
// Data recording for reconnect
this._register(this.onProcessData((e) => this._serializer.handleData(e)));
// Clean up other disposables
this._register(
toDisposable(() => {
for (const e of this._autoReplies.values()) {
e.dispose();
}
this._autoReplies.clear();
}),
);
}
async attach() {
this._logService.trace('persistentTerminalProcess#attach', this._persistentProcessId);
// Something wrong happened if the disconnect runner is not canceled, this likely means
// multiple windows attempted to attach.
if (!(await this._isOrphaned())) {
throw new Error(`Cannot attach to persistent process "${this._persistentProcessId}", it is already adopted`);
}
if (!this._disconnectRunner1.isScheduled() && !this._disconnectRunner2.isScheduled()) {
this._logService.warn(
`Persistent process "${this._persistentProcessId}": Process had no disconnect runners but was an orphan`,
);
}
this._disconnectRunner1.cancel();
this._disconnectRunner2.cancel();
}
async detach(forcePersist) {
this._logService.trace('persistentTerminalProcess#detach', this._persistentProcessId, forcePersist);
// Keep the process around if it was indicated to persist and it has had some iteraction or
// was replayed
if (
this.shouldPersistTerminal &&
(this._interactionState.value !== 'None' /* InteractionState.None */ || forcePersist)
) {
this._disconnectRunner1.schedule();
} else {
this.shutdown(true);
}
}
serializeNormalBuffer() {
return this._serializer.generateReplayEvent(
true,
this._interactionState.value !== 'Session' /* InteractionState.Session */,
);
}
async refreshProperty(type) {
return this._terminalProcess.refreshProperty(type);
}
async updateProperty(type, value) {
if (type === 'fixedDimensions' /* ProcessPropertyType.FixedDimensions */) {
return this._setFixedDimensions(value);
}
}
async start() {
this._logService.trace('persistentTerminalProcess#start', this._persistentProcessId, this._isStarted);
if (!this._isStarted) {
const result = await this._terminalProcess.start();
if (result) {
// it's a terminal launch error
return result;
}
this._isStarted = true;
// If the process was revived, trigger a replay on first start. An alternative approach
// could be to start it on the pty host before attaching but this fails on Windows as
// conpty's inherit cursor option which is required, ends up sending DSR CPR which
// causes conhost to hang when no response is received from the terminal (which wouldn't
// be attached yet). https://github.com/microsoft/terminal/issues/11213
if (this._wasRevived) {
this.triggerReplay();
} else {
this._onPersistentProcessReady.fire();
}
} else {
this._onProcessReady.fire({
pid: this._pid,
cwd: this._cwd,
requiresWindowsMode: isWindows && getWindowsBuildNumber() < 21376,
});
this._onDidChangeProperty.fire({
type: 'title' /* ProcessPropertyType.Title */,
value: this._terminalProcess.currentTitle,
});
this._onDidChangeProperty.fire({
type: 'shellType' /* ProcessPropertyType.ShellType */,
value: this._terminalProcess.shellType,
});
this.triggerReplay();
}
return undefined;
}
shutdown(immediate) {
return this._terminalProcess.shutdown(immediate);
}
input(data) {
this._interactionState.setValue('Session' /* InteractionState.Session */, 'input');
this._serializer.freeRawReviveBuffer();
if (this._inReplay) {
return;
}
for (const listener of this._autoReplies.values()) {
listener.handleInput();
}
return this._terminalProcess.input(data);
}
writeBinary(data) {
return this._terminalProcess.processBinary(data);
}
resize(cols, rows) {
if (this._inReplay) {
return;
}
this._serializer.handleResize(cols, rows);
// Buffered events should flush when a resize occurs
this._bufferer.flushBuffer(this._persistentProcessId);
for (const listener of this._autoReplies.values()) {
listener.handleResize();
}
return this._terminalProcess.resize(cols, rows);
}
setUnicodeVersion(version) {
this.unicodeVersion = version;
this._serializer.setUnicodeVersion?.(version);
// TODO: Pass in unicode version in ctor
}
acknowledgeDataEvent(charCount) {
if (this._inReplay) {
return;
}
return this._terminalProcess.acknowledgeDataEvent(charCount);
}
getInitialCwd() {
return this._terminalProcess.getInitialCwd();
}
getCwd() {
return this._terminalProcess.getCwd();
}
getLatency() {
return this._terminalProcess.getLatency();
}
async triggerReplay() {
if (this._interactionState.value === 'None' /* InteractionState.None */) {
this._interactionState.setValue('ReplayOnly' /* InteractionState.ReplayOnly */, 'triggerReplay');
}
const ev = await this._serializer.generateReplayEvent();
let dataLength = 0;
for (const e of ev.events) {
dataLength += e.data.length;
}
this._logService.info(
`Persistent process "${this._persistentProcessId}": Replaying ${dataLength} chars and ${ev.events.length} size events`,
);
this._onProcessReplay.fire(ev);
this._terminalProcess.clearUnacknowledgedChars();
this._onPersistentProcessReady.fire();
}
installAutoReply(match, reply) {
this._autoReplies.get(match)?.dispose();
this._autoReplies.set(match, new TerminalAutoResponder(this._terminalProcess, match, reply, this._logService));
}
uninstallAutoReply(match) {
const autoReply = this._autoReplies.get(match);
autoReply?.dispose();
this._autoReplies.delete(match);
}
sendCommandResult(reqId, isError, serializedPayload) {
const data = this._pendingCommands.get(reqId);
if (!data) {
return;
}
this._pendingCommands.delete(reqId);
}
orphanQuestionReply() {
this._orphanQuestionReplyTime = Date.now();
if (this._orphanQuestionBarrier) {
const barrier = this._orphanQuestionBarrier;
this._orphanQuestionBarrier = null;
barrier.open();
}
}
reduceGraceTime() {
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() {
return await this._orphanRequestQueue.queue(async () => this._isOrphaned());
}
async _isOrphaned() {
// The process is already known to be orphaned
if (this._disconnectRunner1.isScheduled() || this._disconnectRunner2.isScheduled()) {
return true;
}
// Ask whether the renderer(s) whether the process is orphaned and await the reply
if (!this._orphanQuestionBarrier) {
// the barrier opens after 4 seconds with or without a reply
this._orphanQuestionBarrier = new AutoOpenBarrier(4000);
this._orphanQuestionReplyTime = 0;
this._onProcessOrphanQuestion.fire();
}
await this._orphanQuestionBarrier.wait();
return Date.now() - this._orphanQuestionReplyTime > 500;
}
}
class MutationLogger {
_name;
_value;
_logService;
get value() {
return this._value;
}
setValue(value, reason) {
if (this._value !== value) {
this._value = value;
this._log(reason);
}
}
constructor(_name, _value, _logService) {
this._name = _name;
this._value = _value;
this._logService = _logService;
this._log('initialized');
}
_log(reason) {
this._logService.debug(`MutationLogger "${this._name}" set to "${this._value}", reason: ${reason}`);
}
}
class XtermSerializer {
_rawReviveBuffer;
_xterm;
_shellIntegrationAddon;
_unicodeAddon;
constructor(cols, rows, scrollback, unicodeVersion, reviveBufferWithRestoreMessage, _rawReviveBuffer, logService) {
this._rawReviveBuffer = _rawReviveBuffer;
this._xterm = new XtermTerminal({
cols,
rows,
scrollback,
allowProposedApi: true,
});
if (reviveBufferWithRestoreMessage) {
this._xterm.writeln(reviveBufferWithRestoreMessage);
}
this.setUnicodeVersion(unicodeVersion);
this._shellIntegrationAddon = new ShellIntegrationAddon(true, undefined, logService);
this._xterm.loadAddon(this._shellIntegrationAddon);
}
freeRawReviveBuffer() {
// Free the memory of the terminal if it will need to be re-serialized
this._rawReviveBuffer = undefined;
}
handleData(data) {
this._xterm.write(data);
}
handleResize(cols, rows) {
this._xterm.resize(cols, rows);
}
async generateReplayEvent(normalBufferOnly, restoreToLastReviveBuffer) {
const serialize = new (await this._getSerializeConstructor())();
this._xterm.loadAddon(serialize);
const options = {
scrollback: this._xterm.options.scrollback,
};
if (normalBufferOnly) {
options.excludeAltBuffer = true;
options.excludeModes = true;
}
let serialized;
if (restoreToLastReviveBuffer && this._rawReviveBuffer) {
serialized = this._rawReviveBuffer;
} else {
serialized = serialize.serialize(options);
}
return {
events: [
{
cols: this._xterm.cols,
rows: this._xterm.rows,
data: serialized,
},
],
commands: this._shellIntegrationAddon.serialize(),
};
}
async setUnicodeVersion(version) {
if (this._xterm.unicode.activeVersion === version) {
return;
}
if (version === '11') {
this._unicodeAddon = new (await this._getUnicode11Constructor())();
this._xterm.loadAddon(this._unicodeAddon);
} else {
this._unicodeAddon?.dispose();
this._unicodeAddon = undefined;
}
this._xterm.unicode.activeVersion = version;
}
async _getUnicode11Constructor() {
if (!Unicode11Addon) {
Unicode11Addon = (await import('xterm-addon-unicode11')).Unicode11Addon;
}
return Unicode11Addon;
}
async _getSerializeConstructor() {
if (!SerializeAddon) {
SerializeAddon = (await import('xterm-addon-serialize')).SerializeAddon;
}
return SerializeAddon;
}
}
function printTime(ms) {
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}`;
}