Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ import { ILoggerMainService } from '../../platform/log/electron-main/loggerServi
import { IInitialProtocolUrls, IProtocolUrl } from '../../platform/url/electron-main/url.js';
import { IUtilityProcessWorkerMainService, UtilityProcessWorkerMainService } from '../../platform/utilityProcess/electron-main/utilityProcessWorkerMainService.js';
import { ipcUtilityProcessWorkerChannelName } from '../../platform/utilityProcess/common/utilityProcessWorkerService.js';
import { ILocalPtyService, LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from '../../platform/terminal/common/terminal.js';
import { ILocalPtyService, LocalReconnectConstants, localPtyServiceUnbufferedEvents, TerminalIpcChannels, TerminalSettingId } from '../../platform/terminal/common/terminal.js';
import { ElectronPtyHostStarter } from '../../platform/terminal/electron-main/electronPtyHostStarter.js';
import { PtyHostService } from '../../platform/terminal/node/ptyHostService.js';
import { ElectronAgentHostStarter } from '../../platform/agentHost/electron-main/electronAgentHostStarter.js';
Expand Down Expand Up @@ -1444,7 +1444,9 @@ export class CodeApplication extends Disposable {
sharedProcessClient.then(client => client.registerChannel('profileStorageListener', profileStorageListener));

// Terminal
const ptyHostChannel = ProxyChannel.fromService(accessor.get(ILocalPtyService), disposables);
const ptyHostChannel = ProxyChannel.fromService(accessor.get(ILocalPtyService), disposables, {
unbufferedEvents: localPtyServiceUnbufferedEvents
});
mainProcessElectronServer.registerChannel(TerminalIpcChannels.LocalPty, ptyHostChannel);

// External Terminal
Expand Down
11 changes: 11 additions & 0 deletions src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,17 @@ Registry.add(TerminalExtensions.Backend, new TerminalBackendRegistry());

export const ILocalPtyService = createDecorator<ILocalPtyService>('localPtyService');

// Renderers consume these events over the direct pty host connection, not the localPty channel.
export const localPtyServiceUnbufferedEvents: readonly (keyof IPtyService)[] = [
'onProcessData',
'onProcessReady',
'onProcessReplay',
'onProcessOrphanQuestion',
'onDidRequestDetach',
'onDidChangeProperty',
'onProcessExit'
];

/**
* A service responsible for communicating with the pty host process on Electron.
*
Expand Down
52 changes: 50 additions & 2 deletions src/vs/platform/terminal/test/node/ptyHostService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,66 @@
*--------------------------------------------------------------------------------------------*/

import { deepStrictEqual } from 'assert';
import { Event } from '../../../../base/common/event.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { IChannel, IChannelClient } from '../../../../base/parts/ipc/common/ipc.js';
import { IChannel, IChannelClient, ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js';
import { NullLogService, NullLoggerService } from '../../../log/common/log.js';
import { localPtyServiceUnbufferedEvents } from '../../common/terminal.js';
import { IPtyHostConnection, IPtyHostStarter } from '../../node/ptyHost.js';
import { PtyHostService } from '../../node/ptyHostService.js';

suite('PtyHostService', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();

suite('localPty channel', () => {
for (const eventName of ['onProcessData', 'onProcessReady', 'onProcessReplay', 'onProcessOrphanQuestion', 'onDidRequestDetach', 'onDidChangeProperty', 'onProcessExit']) {
test(`${eventName} only subscribes while an IPC listener is attached`, () => {
const emitter = store.add(new Emitter<string>());
const channel = ProxyChannel.fromService({ [eventName]: emitter.event }, store, {
unbufferedEvents: localPtyServiceUnbufferedEvents
});
const onEvent = channel.listen<string>(undefined, eventName);
const listenerStates = [emitter.hasListeners()];
const messages: string[] = [];

emitter.fire('before');
const firstListener = store.add(onEvent(e => messages.push(e)));
listenerStates.push(emitter.hasListeners());
emitter.fire('first');
firstListener.dispose();
listenerStates.push(emitter.hasListeners());

emitter.fire('between');
const secondListener = store.add(onEvent(e => messages.push(e)));
listenerStates.push(emitter.hasListeners());
emitter.fire('second');
secondListener.dispose();
listenerStates.push(emitter.hasListeners());
emitter.fire('after');

deepStrictEqual({ listenerStates, messages }, {
listenerStates: [false, true, false, true, false],
messages: ['first', 'second']
});
});
}

for (const eventName of ['onPtyHostExit', 'onPtyHostStart', 'onPtyHostUnresponsive', 'onPtyHostResponsive', 'onPtyHostRequestResolveVariables']) {
test(`${eventName} remains buffered until an IPC listener attaches`, async () => {
const emitter = store.add(new Emitter<number>());
const channel = ProxyChannel.fromService({ [eventName]: emitter.event }, store, {
unbufferedEvents: localPtyServiceUnbufferedEvents
});

emitter.fire(1);

deepStrictEqual(await Event.toPromise(channel.listen<number>(undefined, eventName)), 1);
});
}
});

test('restartPtyHost disposes listeners registered during pty host startup', async () => {
// Track active listener counts per event across pty host restarts. Without the
// fix, each restart would leak the listeners registered in _startPtyHost.
Expand Down