From 8553ed9d439c1a6c9056f37aa359de5d5dcc6834 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 13:29:41 -0700 Subject: [PATCH] Clean up runtime environment IPC registration --- src/main/ipc/runtime-environments.test.ts | 29 ++++++++++++++++++++++- src/main/ipc/runtime-environments.ts | 18 ++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/ipc/runtime-environments.test.ts b/src/main/ipc/runtime-environments.test.ts index 14d519533..350099e37 100644 --- a/src/main/ipc/runtime-environments.test.ts +++ b/src/main/ipc/runtime-environments.test.ts @@ -10,6 +10,8 @@ import * as environmentStore from '../../shared/runtime-environment-store' const { handleMock, onMock, + removeHandlerMock, + removeAllListenersMock, getPathMock, sendRemoteRuntimeRequestMock, subscribeRemoteRuntimeRequestMock, @@ -18,6 +20,8 @@ const { } = vi.hoisted(() => ({ handleMock: vi.fn(), onMock: vi.fn(), + removeHandlerMock: vi.fn(), + removeAllListenersMock: vi.fn(), getPathMock: vi.fn(), sendRemoteRuntimeRequestMock: vi.fn(), subscribeRemoteRuntimeRequestMock: vi.fn(), @@ -27,7 +31,12 @@ const { vi.mock('electron', () => ({ app: { getPath: getPathMock }, - ipcMain: { handle: handleMock, on: onMock } + ipcMain: { + handle: handleMock, + on: onMock, + removeHandler: removeHandlerMock, + removeAllListeners: removeAllListenersMock + } })) vi.mock('../../shared/remote-runtime-client', () => ({ @@ -68,6 +77,8 @@ describe('registerRuntimeEnvironmentHandlers', () => { getPathMock.mockReturnValue(userDataPath) handleMock.mockReset() onMock.mockReset() + removeHandlerMock.mockReset() + removeAllListenersMock.mockReset() sendRemoteRuntimeRequestMock.mockReset() subscribeRemoteRuntimeRequestMock.mockReset() sendRemoteRuntimeConnectionRequestMock.mockReset() @@ -96,6 +107,22 @@ describe('registerRuntimeEnvironmentHandlers', () => { ]) }) + it('clears stale IPC registrations before registering runtime environment handlers', () => { + registerRuntimeEnvironmentHandlers() + + expect(removeHandlerMock.mock.calls.map((call) => call[0])).toEqual([ + 'runtimeEnvironments:list', + 'runtimeEnvironments:addFromPairingCode', + 'runtimeEnvironments:resolve', + 'runtimeEnvironments:remove', + 'runtimeEnvironments:getStatus', + 'runtimeEnvironments:call', + 'runtimeEnvironments:subscribe', + 'runtimeEnvironments:unsubscribe' + ]) + expect(removeAllListenersMock).toHaveBeenCalledWith('runtimeEnvironments:subscriptionBinary') + }) + it('stores, resolves, lists, and removes environments under Electron userData', async () => { registerRuntimeEnvironmentHandlers() diff --git a/src/main/ipc/runtime-environments.ts b/src/main/ipc/runtime-environments.ts index 9b278ba63..ca7e54ea7 100644 --- a/src/main/ipc/runtime-environments.ts +++ b/src/main/ipc/runtime-environments.ts @@ -28,6 +28,17 @@ import { } from './runtime-environment-request-connections' const DEFAULT_REMOTE_RUNTIME_TIMEOUT_MS = 15_000 +const RUNTIME_ENVIRONMENT_HANDLER_CHANNELS = [ + 'runtimeEnvironments:list', + 'runtimeEnvironments:addFromPairingCode', + 'runtimeEnvironments:resolve', + 'runtimeEnvironments:remove', + 'runtimeEnvironments:getStatus', + 'runtimeEnvironments:call', + 'runtimeEnvironments:subscribe', + 'runtimeEnvironments:unsubscribe' +] as const + type RetainedRemoteRuntimeSubscription = RemoteRuntimeSubscription & { ownerWebContentsId: number removeDestroyedListener: () => void @@ -43,6 +54,13 @@ function shouldUseCachedRequestConnection(method: string): boolean { } export function registerRuntimeEnvironmentHandlers(): void { + // Why: keep direct re-registration safe even though register-core-handlers + // normally guards this path; otherwise the binary send listener can stack. + for (const channel of RUNTIME_ENVIRONMENT_HANDLER_CHANNELS) { + ipcMain.removeHandler(channel) + } + ipcMain.removeAllListeners('runtimeEnvironments:subscriptionBinary') + ipcMain.handle('runtimeEnvironments:list', (): PublicKnownRuntimeEnvironment[] => listEnvironments(getUserDataPath()).map(redactRuntimeEnvironment) )