From be69a8d043b8fef583f78e4271ab29a44d3f8776 Mon Sep 17 00:00:00 2001 From: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:45:03 +0800 Subject: [PATCH] fix(desktop): tear down SSH lifecycle when a pooled backend is stopped Idle-reaper and LRU eviction only called stopPoolBackend, which left sshConnections and the keep-alive WS armed on process-less remote descriptors. --- apps/desktop/electron/main.ts | 5 ++++ .../electron/ssh-isolated-keepalive.test.ts | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index a4ae90e9cd..887f430ee9 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -12744,6 +12744,11 @@ async function stopPoolBackend(profile: string) { const entry = backendPool.get(profile) await poolStopper.stop(profile) releaseLocalBackendSlot(entry) + // Remote / SSH-isolated pool entries keep `process: null`. Evicting the + // descriptor alone leaves sshConnections + the keep-alive WS armed, so + // idle-reaper / LRU retirement would pin the tunnel indefinitely (#106935). + await sshBootstrapCoordinator.cancelAndWait(profile) + await teardownSshConnection(profile) } async function teardownPoolBackendAndWait(profile) { diff --git a/apps/desktop/electron/ssh-isolated-keepalive.test.ts b/apps/desktop/electron/ssh-isolated-keepalive.test.ts index 963a6762c8..6f931912e4 100644 --- a/apps/desktop/electron/ssh-isolated-keepalive.test.ts +++ b/apps/desktop/electron/ssh-isolated-keepalive.test.ts @@ -70,6 +70,29 @@ describe('main.ts wiring for SSH-isolated keep-alive WS (#106935)', () => { expect(body).toMatch(/sshIsolatedKeepalives\.stop\(\s*scope/) }) + it('stopPoolBackend tears down the matching SSH lifecycle (idle-reaper / LRU)', () => { + const fnStart = mainSource.indexOf('async function stopPoolBackend(') + expect(fnStart).toBeGreaterThan(-1) + + const nextFn = mainSource.indexOf('\nasync function teardownPoolBackendAndWait(', fnStart + 1) + const body = mainSource.slice(fnStart, nextFn === -1 ? fnStart + 400 : nextFn) + + expect(body).toMatch(/sshBootstrapCoordinator\.cancelAndWait\(\s*profile/) + expect(body).toMatch(/teardownSshConnection\(\s*profile/) + }) + + it('idle reaper retires descriptors only through stopPoolBackend', () => { + const fnStart = mainSource.indexOf('function startPoolIdleReaper(') + expect(fnStart).toBeGreaterThan(-1) + + const nextFn = mainSource.indexOf('\nfunction releaseLocalBackendSlot(', fnStart + 1) + const body = mainSource.slice(fnStart, nextFn === -1 ? fnStart + 800 : nextFn) + + expect(body).toContain('stopPoolBackend(profile)') + expect(body).not.toMatch(/backendPool\.delete/) + expect(body).not.toMatch(/sshConnections\.delete/) + }) + it('imports the keep-alive registry from the electron helper (not inline in main)', () => { expect(mainSource).toMatch(/createSshIsolatedKeepaliveRegistry/) expect(mainSource).toMatch(/from '\.\/ssh-isolated-keepalive'/)