From e2db4ea0c76f47983c5e67eb425ef03cc949848a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:17:03 +0000 Subject: [PATCH 1/2] Initial plan From 609ba57ff8f22b276344ea4b10a2af4d7f5c07ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:27:21 +0000 Subject: [PATCH 2/2] Fix unbounded terminal event buffering on the localPty channel Co-authored-by: ulugbekna <16353531+ulugbekna@users.noreply.github.com> --- src/vs/code/electron-main/app.ts | 6 ++- src/vs/platform/terminal/common/terminal.ts | 11 ++++ .../terminal/test/node/ptyHostService.test.ts | 52 ++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index c9736ff355fa2..0d7278b3e6c9c 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -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'; @@ -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 diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index cca3805245ee0..dc36faa884a8f 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -1250,6 +1250,17 @@ Registry.add(TerminalExtensions.Backend, new TerminalBackendRegistry()); export const ILocalPtyService = createDecorator('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. * diff --git a/src/vs/platform/terminal/test/node/ptyHostService.test.ts b/src/vs/platform/terminal/test/node/ptyHostService.test.ts index 5e714085db777..267666cd2f395 100644 --- a/src/vs/platform/terminal/test/node/ptyHostService.test.ts +++ b/src/vs/platform/terminal/test/node/ptyHostService.test.ts @@ -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()); + const channel = ProxyChannel.fromService({ [eventName]: emitter.event }, store, { + unbufferedEvents: localPtyServiceUnbufferedEvents + }); + const onEvent = channel.listen(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()); + const channel = ProxyChannel.fromService({ [eventName]: emitter.event }, store, { + unbufferedEvents: localPtyServiceUnbufferedEvents + }); + + emitter.fire(1); + + deepStrictEqual(await Event.toPromise(channel.listen(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.