src/vs/platform/terminal/node/ptyHostService.ts

432 LOC · 254 covered · 178 uncovered · 54 ranges · 1 concepts · 1 introducers · 1 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- ptyHostService.ts ×54
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { Emitter, Event } from '../../../base/common/event.js';
7 > import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
8 > import { IProcessEnvironment, OS, OperatingSystem, isWindows } from '../../../base/common/platform.js';
9 > import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
10 > import { IConfigurationService } from '../../configuration/common/configuration.js';
11 > import { ILogService, ILoggerService, LogLevel } from '../../log/common/log.js';
12 > import { RemoteLoggerChannelClient } from '../../log/common/logIpc.js';
13 > import { getResolvedShellEnv } from '../../shell/node/shellEnv.js';
14 > import { IPtyHostProcessReplayEvent } from '../common/capabilities/capabilities.js';
15 > import { RequestStore } from '../common/requestStore.js';
16 > import { HeartbeatConstants, IHeartbeatService, ITerminalLaunchResult, IProcessDataEvent, IProcessProperty, IProcessPropertyMap, IProcessReadyEvent, IPtyHostLatencyMeasurement, IPtyHostService, IPtyService, IRequestResolveVariablesEvent, ISerializedTerminalState, IShellLaunchConfig, ITerminalLaunchError, ITerminalProcessOptions, ITerminalProfile, ITerminalsLayoutInfo, ProcessPropertyType, TerminalIcon, TerminalIpcChannels, TerminalSettingId, TitleEventSource } from '../common/terminal.js';
17 > import { registerTerminalPlatformConfiguration } from '../common/terminalPlatformConfiguration.js';
18 > import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from '../common/terminalProcess.js';
19 > import { IPtyHostConnection, IPtyHostStarter } from './ptyHost.js';
20 > import { detectAvailableProfiles } from './terminalProfiles.js';
21 > import * as performance from '../../../base/common/performance.js';
22 > import { getSystemShell } from '../../../base/node/shell.js';
23 > import { StopWatch } from '../../../base/common/stopwatch.js';
24 >
25 > enum Constants {
26 > MaxRestarts = 5
27 > }
28 >
29 > /**
30 > * This service implements IPtyService by launching a pty host process, forwarding messages to and
31 > * from the pty host process and manages the connection.
32 > */
33 > export class PtyHostService extends Disposable implements IPtyHostService {
34 > declare readonly _serviceBrand: undefined;
35 >
36 > private __connection?: IPtyHostConnection;
37 > // ProxyChannel is not used here because events get lost when forwarding across multiple proxies
38 > private __proxy?: IPtyService;
39 >
40 > private get _proxy(): IPtyService {
41 > this._ensurePtyHost();
42 > return this.__proxy!;
43 > }
44 > /**
45 > * Get the proxy if it exists, otherwise undefined. This is used when calls are not needed to be
46 > * passed through to the pty host if it has not yet been spawned.
47 > */
48 > private get _optionalProxy(): IPtyService | undefined {
49 > return this.__proxy;
50 > }
51 >
52 > private _ensurePtyHost() {
53 if (!this.__connection) {
54 this._startPtyHost();
55 }
56 }
58 > private readonly _resolveVariablesRequestStore: RequestStore<string[], { workspaceId: string; originalText: string[] }>;
59 > private _wasQuitRequested = false;
60 > private _restartCount = 0;
61 > private _isResponsive = true;
62 > private _heartbeatFirstTimeout?: Timeout;
63 > private _heartbeatSecondTimeout?: Timeout;
64 >
65 > private readonly _onPtyHostExit = this._register(new Emitter<number>());
66 > readonly onPtyHostExit = this._onPtyHostExit.event;
67 > private readonly _onPtyHostStart = this._register(new Emitter<void>());
68 > readonly onPtyHostStart = this._onPtyHostStart.event;
69 > private readonly _onPtyHostUnresponsive = this._register(new Emitter<void>());
70 > readonly onPtyHostUnresponsive = this._onPtyHostUnresponsive.event;
71 > private readonly _onPtyHostResponsive = this._register(new Emitter<void>());
72 > readonly onPtyHostResponsive = this._onPtyHostResponsive.event;
73 > private readonly _onPtyHostRequestResolveVariables = this._register(new Emitter<IRequestResolveVariablesEvent>());
74 > readonly onPtyHostRequestResolveVariables = this._onPtyHostRequestResolveVariables.event;
75 >
76 > private readonly _onProcessData = this._register(new Emitter<{ id: number; event: IProcessDataEvent | string }>());
77 > readonly onProcessData = this._onProcessData.event;
78 > private readonly _onProcessReady = this._register(new Emitter<{ id: number; event: IProcessReadyEvent }>());
79 > readonly onProcessReady = this._onProcessReady.event;
80 > private readonly _onProcessReplay = this._register(new Emitter<{ id: number; event: IPtyHostProcessReplayEvent }>());
81 > readonly onProcessReplay = this._onProcessReplay.event;
82 > private readonly _onProcessOrphanQuestion = this._register(new Emitter<{ id: number }>());
83 > readonly onProcessOrphanQuestion = this._onProcessOrphanQuestion.event;
84 > private readonly _onDidRequestDetach = this._register(new Emitter<{ requestId: number; workspaceId: string; instanceId: number }>());
85 > readonly onDidRequestDetach = this._onDidRequestDetach.event;
86 > private readonly _onDidChangeProperty = this._register(new Emitter<{ id: number; property: IProcessProperty }>());
87 > readonly onDidChangeProperty = this._onDidChangeProperty.event;
88 > private readonly _onProcessExit = this._register(new Emitter<{ id: number; event: number | undefined }>());
89 > readonly onProcessExit = this._onProcessExit.event;
90 >
91 > private readonly _ptyHostStore = this._register(new DisposableStore());
92 >
93 > constructor(
94 > private readonly _ptyHostStarter: IPtyHostStarter,
95 > @IConfigurationService private readonly _configurationService: IConfigurationService,
96 > @ILogService private readonly _logService: ILogService,
97 > @ILoggerService private readonly _loggerService: ILoggerService,
98 > ) {
99 > super();
100 >
101 > // Platform configuration is required on the process running the pty host (shared process or
102 > // remote server).
103 > registerTerminalPlatformConfiguration();
104 >
105 > this._register(this._ptyHostStarter);
106 > this._register(toDisposable(() => this._disposePtyHost()));
107 >
108 > this._resolveVariablesRequestStore = this._register(new RequestStore(undefined, this._logService));
109 > this._register(this._resolveVariablesRequestStore.onCreateRequest(this._onPtyHostRequestResolveVariables.fire, this._onPtyHostRequestResolveVariables));
110 >
111 > // Start the pty host when a window requests a connection, if the starter has that capability.
112 > if (this._ptyHostStarter.onRequestConnection) {
113 this._register(Event.once(this._ptyHostStarter.onRequestConnection)(() => this._ensurePtyHost()));
114 }
116 > if (this._ptyHostStarter.onWillShutdown) {
117 this._register(this._ptyHostStarter.onWillShutdown(() => this._wasQuitRequested = true));
118 }
120 >
121 > private get _ignoreProcessNames(): string[] {
122 > return this._configurationService.getValue<string[]>(TerminalSettingId.IgnoreProcessNames);
123 > }
124 >
125 > private async _refreshIgnoreProcessNames(): Promise<void> {
126 > return this._optionalProxy?.refreshIgnoreProcessNames?.(this._ignoreProcessNames);
127 > }
128 >
129 > private async _resolveShellEnv(): Promise<typeof process.env> {
130 if (isWindows) {
131 return process.env;
132 }
133
134 try {
135 return await getResolvedShellEnv(this._configurationService, this._logService, { _: [] }, process.env);
136 } catch (error) {
137 this._logService.error('ptyHost was unable to resolve shell environment', error);
138
139 return {};
140 }
141 }
143 > private _startPtyHost(): void {
144 > const connection = this._ptyHostStarter.start();
145 > const client = connection.client;
146 > const store = this._ptyHostStore;
147 > // Transfer ownership of the per-host connection store so it is disposed together with the listeners below on the next restart.
148 > store.add(connection.store);
149 >
150 > // Log a full stack trace which will tell the exact reason the pty host is starting up
151 > if (this._logService.getLevel() === LogLevel.Trace) {
152 this._logService.trace('PtyHostService#_startPtyHost', new Error().stack?.replace(/^Error/, ''));
153 }
155 > // Setup heartbeat service and trigger a heartbeat immediately to reset the timeouts
156 > const heartbeatService = ProxyChannel.toService<IHeartbeatService>(client.getChannel(TerminalIpcChannels.Heartbeat));
157 > store.add(heartbeatService.onBeat(() => this._handleHeartbeat()));
158 > this._handleHeartbeat(true);
159 >
160 > // Handle exit
161 > store.add(connection.onDidProcessExit(e => {
162 this._onPtyHostExit.fire(e.code);
163 if (!this._wasQuitRequested && !this._store.isDisposed) {
164 if (this._restartCount <= Constants.MaxRestarts) {
165 this._logService.error(`ptyHost terminated unexpectedly with code ${e.code}`);
166 this._restartCount++;
167 this.restartPtyHost();
168 } else {
169 this._logService.error(`ptyHost terminated unexpectedly with code ${e.code}, giving up`);
170 }
171 }
173 >
174 > // Create proxy and forward events
175 > const proxy = ProxyChannel.toService<IPtyService>(client.getChannel(TerminalIpcChannels.PtyHost));
176 > store.add(proxy.onProcessData(e => this._onProcessData.fire(e)));
177 > store.add(proxy.onProcessReady(e => this._onProcessReady.fire(e)));
178 > store.add(proxy.onProcessExit(e => this._onProcessExit.fire(e)));
179 > store.add(proxy.onDidChangeProperty(e => this._onDidChangeProperty.fire(e)));
180 > store.add(proxy.onProcessReplay(e => this._onProcessReplay.fire(e)));
181 > store.add(proxy.onProcessOrphanQuestion(e => this._onProcessOrphanQuestion.fire(e)));
182 > store.add(proxy.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)));
183 >
184 > store.add(new RemoteLoggerChannelClient(this._loggerService, client.getChannel(TerminalIpcChannels.Logger)));
185 >
186 > this.__connection = connection;
187 > this.__proxy = proxy;
188 >
189 > this._onPtyHostStart.fire();
190 >
191 > store.add(this._configurationService.onDidChangeConfiguration(async e => {
192 if (e.affectsConfiguration(TerminalSettingId.IgnoreProcessNames)) {
193 await this._refreshIgnoreProcessNames();
194 }
196 > this._refreshIgnoreProcessNames();
197 > }
198 >
199 > async createProcess(
200 shellLaunchConfig: IShellLaunchConfig,
201 cwd: string,
202 cols: number,
203 rows: number,
204 unicodeVersion: '6' | '11',
205 env: IProcessEnvironment,
206 executableEnv: IProcessEnvironment,
207 options: ITerminalProcessOptions,
208 shouldPersist: boolean,
209 workspaceId: string,
210 workspaceName: string
211 ): Promise<number> {
212 const timeout = setTimeout(() => this._handleUnresponsiveCreateProcess(), HeartbeatConstants.CreateProcessTimeout);
213 const id = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, options, shouldPersist, workspaceId, workspaceName);
214 clearTimeout(timeout);
215 return id;
216 }
217 > updateTitle(id: number, title: string, titleSource: TitleEventSource): Promise<void> { ptyHostService.ts ×54
218 return this._proxy.updateTitle(id, title, titleSource);
219 }
220 > updateIcon(id: number, userInitiated: boolean, icon: TerminalIcon, color?: string): Promise<void> { ptyHostService.ts ×54
221 return this._proxy.updateIcon(id, userInitiated, icon, color);
222 }
223 > attachToProcess(id: number): Promise<void> { ptyHostService.ts ×54
224 return this._proxy.attachToProcess(id);
225 }
226 > detachFromProcess(id: number, forcePersist?: boolean): Promise<void> { ptyHostService.ts ×54
227 return this._proxy.detachFromProcess(id, forcePersist);
228 }
229 > shutdownAll(): Promise<void> { ptyHostService.ts ×54
230 return this._proxy.shutdownAll();
231 }
232 > listProcesses(): Promise<IProcessDetails[]> { ptyHostService.ts ×54
233 return this._proxy.listProcesses();
234 }
235 > async getPerformanceMarks(): Promise<performance.PerformanceMark[]> { ptyHostService.ts ×54
236 return this._optionalProxy?.getPerformanceMarks() ?? [];
237 }
238 > async reduceConnectionGraceTime(): Promise<void> { ptyHostService.ts ×54
239 return this._optionalProxy?.reduceConnectionGraceTime();
240 }
241 > start(id: number): Promise<ITerminalLaunchError | ITerminalLaunchResult | undefined> { ptyHostService.ts ×54
242 return this._proxy.start(id);
243 }
244 > shutdown(id: number, immediate: boolean): Promise<void> { ptyHostService.ts ×54
245 return this._proxy.shutdown(id, immediate);
246 }
247 > input(id: number, data: string): Promise<void> { ptyHostService.ts ×54
248 return this._proxy.input(id, data);
249 }
250 > sendSignal(id: number, signal: string): Promise<void> { ptyHostService.ts ×54
251 return this._proxy.sendSignal(id, signal);
252 }
253 > processBinary(id: number, data: string): Promise<void> { ptyHostService.ts ×54
254 return this._proxy.processBinary(id, data);
255 }
256 > resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise<void> { ptyHostService.ts ×54
257 return this._proxy.resize(id, cols, rows, pixelWidth, pixelHeight);
258 }
259 > clearBuffer(id: number): Promise<void> { ptyHostService.ts ×54
260 return this._proxy.clearBuffer(id);
261 }
262 > acknowledgeDataEvent(id: number, charCount: number): Promise<void> { ptyHostService.ts ×54
263 return this._proxy.acknowledgeDataEvent(id, charCount);
264 }
265 > setUnicodeVersion(id: number, version: '6' | '11'): Promise<void> { ptyHostService.ts ×54
266 return this._proxy.setUnicodeVersion(id, version);
267 }
268 > setNextCommandId(id: number, commandLine: string, commandId: string): Promise<void> { ptyHostService.ts ×54
269 return this._proxy.setNextCommandId(id, commandLine, commandId);
270 }
271 > getInitialCwd(id: number): Promise<string> { ptyHostService.ts ×54
272 return this._proxy.getInitialCwd(id);
273 }
274 > getCwd(id: number): Promise<string> { ptyHostService.ts ×54
275 return this._proxy.getCwd(id);
276 }
277 > async getLatency(): Promise<IPtyHostLatencyMeasurement[]> { ptyHostService.ts ×54
278 const sw = new StopWatch();
279 const results = await this._proxy.getLatency();
280 sw.stop();
281 return [
282 {
283 label: 'ptyhostservice<->ptyhost',
284 latency: sw.elapsed()
285 },
286 ...results
287 ];
288 }
289 > orphanQuestionReply(id: number): Promise<void> { ptyHostService.ts ×54
290 return this._proxy.orphanQuestionReply(id);
291 }
293 > installAutoReply(match: string, reply: string): Promise<void> {
294 return this._proxy.installAutoReply(match, reply);
295 }
296 > uninstallAllAutoReplies(): Promise<void> { ptyHostService.ts ×54
297 return this._proxy.uninstallAllAutoReplies();
298 }
300 > getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string> {
301 return this._optionalProxy?.getDefaultSystemShell(osOverride) ?? getSystemShell(osOverride ?? OS, process.env);
302 }
303 > async getProfiles(workspaceId: string, profiles: unknown, defaultProfile: unknown, includeDetectedProfiles: boolean = false): Promise<ITerminalProfile[]> { ptyHostService.ts ×54
304 const shellEnv = await this._resolveShellEnv();
305 return detectAvailableProfiles(profiles, defaultProfile, includeDetectedProfiles, this._configurationService, shellEnv, undefined, this._logService, this._resolveVariables.bind(this, workspaceId));
306 }
307 > async getEnvironment(): Promise<IProcessEnvironment> { ptyHostService.ts ×54
308 // If the pty host is yet to be launched, just return the environment of this process as it
309 // is essentially the same when used to evaluate terminal profiles.
310 if (!this.__proxy) {
311 return { ...process.env };
312 }
313 return this._proxy.getEnvironment();
314 }
315 > getWslPath(original: string, direction: 'unix-to-win' | 'win-to-unix'): Promise<string> { ptyHostService.ts ×54
316 return this._proxy.getWslPath(original, direction);
317 }
319 > getRevivedPtyNewId(workspaceId: string, id: number): Promise<number | undefined> {
320 return this._proxy.getRevivedPtyNewId(workspaceId, id);
321 }
323 > setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise<void> {
324 return this._proxy.setTerminalLayoutInfo(args);
325 }
326 > async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise<ITerminalsLayoutInfo | undefined> { ptyHostService.ts ×54
327 // This is optional as we want reconnect requests to go through only if the pty host exists.
328 // Revive is handled specially as reviveTerminalProcesses is guaranteed to be called before
329 // the request for layout info.
330 return this._optionalProxy?.getTerminalLayoutInfo(args);
331 }
333 > async requestDetachInstance(workspaceId: string, instanceId: number): Promise<IProcessDetails | undefined> {
334 return this._proxy.requestDetachInstance(workspaceId, instanceId);
335 }
337 > async acceptDetachInstanceReply(requestId: number, persistentProcessId: number): Promise<void> {
338 return this._proxy.acceptDetachInstanceReply(requestId, persistentProcessId);
339 }
341 > async freePortKillProcess(port: string): Promise<{ port: string; processId: string }> {
342 if (!this._proxy.freePortKillProcess) {
343 throw new Error('freePortKillProcess does not exist on the pty proxy');
344 }
345 return this._proxy.freePortKillProcess(port);
346 }
348 > async serializeTerminalState(ids: number[]): Promise<string> {
349 return this._proxy.serializeTerminalState(ids);
350 }
352 > async reviveTerminalProcesses(workspaceId: string, state: ISerializedTerminalState[], dateTimeFormatLocate: string) {
353 return this._proxy.reviveTerminalProcesses(workspaceId, state, dateTimeFormatLocate);
354 }
356 > async refreshProperty<T extends ProcessPropertyType>(id: number, property: T): Promise<IProcessPropertyMap[T]> {
357 return this._proxy.refreshProperty(id, property);
358
359 }
360 > async updateProperty<T extends ProcessPropertyType>(id: number, property: T, value: IProcessPropertyMap[T]): Promise<void> { ptyHostService.ts ×54
361 return this._proxy.updateProperty(id, property, value);
362 }
364 > async restartPtyHost(): Promise<void> {
365 > this._disposePtyHost();
366 > this._isResponsive = true;
367 > this._startPtyHost();
368 > }
369 >
370 > private _disposePtyHost(): void {
371 > // Heartbeat timers are bare setTimeout handles, not disposables in the store, so they need an explicit clear.
372 > // shutdownAll() is fired before clearing the store so any in-flight exit listener still has a live proxy to read from;
373 > // the per-host listener store is cleared last so the on-exit signal isn't dropped on the floor.
374 > this._clearHeartbeatTimeouts();
375 > // Fire-and-forget: the IPC channel may already be gone; swallow rejections so we don't surface an unhandled promise.
376 > this._optionalProxy?.shutdownAll().catch(() => { });
377 > this.__connection = undefined;
378 > this.__proxy = undefined;
379 > this._ptyHostStore.clear();
380 > }
381 >
382 > private _handleHeartbeat(isConnecting?: boolean) {
383 > this._clearHeartbeatTimeouts();
384 > this._heartbeatFirstTimeout = setTimeout(() => this._handleHeartbeatFirstTimeout(), isConnecting ? HeartbeatConstants.ConnectingBeatInterval : (HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier));
385 > if (!this._isResponsive) {
386 this._isResponsive = true;
387 this._onPtyHostResponsive.fire();
388 }
390 >
391 > private _handleHeartbeatFirstTimeout() {
392 this._logService.warn(`No ptyHost heartbeat after ${HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier / 1000} seconds`);
393 this._heartbeatFirstTimeout = undefined;
394 this._heartbeatSecondTimeout = setTimeout(() => this._handleHeartbeatSecondTimeout(), HeartbeatConstants.BeatInterval * HeartbeatConstants.SecondWaitMultiplier);
395 }
397 > private _handleHeartbeatSecondTimeout() {
398 this._logService.error(`No ptyHost heartbeat after ${(HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier + HeartbeatConstants.BeatInterval * HeartbeatConstants.FirstWaitMultiplier) / 1000} seconds`);
399 this._heartbeatSecondTimeout = undefined;
400 if (this._isResponsive) {
401 this._isResponsive = false;
402 this._onPtyHostUnresponsive.fire();
403 }
404 }
406 > private _handleUnresponsiveCreateProcess() {
407 this._clearHeartbeatTimeouts();
408 this._logService.error(`No ptyHost response to createProcess after ${HeartbeatConstants.CreateProcessTimeout / 1000} seconds`);
409 if (this._isResponsive) {
410 this._isResponsive = false;
411 this._onPtyHostUnresponsive.fire();
412 }
413 }
415 > private _clearHeartbeatTimeouts() {
416 > if (this._heartbeatFirstTimeout) {
417 > clearTimeout(this._heartbeatFirstTimeout);
418 > this._heartbeatFirstTimeout = undefined;
419 > }
420 > if (this._heartbeatSecondTimeout) {
421 clearTimeout(this._heartbeatSecondTimeout);
422 this._heartbeatSecondTimeout = undefined;
423 }
425 >
426 > private _resolveVariables(workspaceId: string, text: string[]): Promise<string[]> {
427 return this._resolveVariablesRequestStore.createRequest({ workspaceId, originalText: text });
428 }
429 > async acceptPtyHostResolvedVariables(requestId: number, resolved: string[]) { ptyHostService.ts ×54
430 this._resolveVariablesRequestStore.acceptReply(requestId, resolved);
431 }