diff --git a/src/main/ipc/ssh.test.ts b/src/main/ipc/ssh.test.ts index 37517cbe30b..0c5a51d9ae2 100644 --- a/src/main/ipc/ssh.test.ts +++ b/src/main/ipc/ssh.test.ts @@ -1468,6 +1468,41 @@ describe('SSH IPC handlers', () => { expect(mockConnectionManager.reconnect).not.toHaveBeenCalled() }) + it('does not reconnect after resume when the target was disconnected during the probe', async () => { + const target: SshTarget = { + id: 'ssh-1', + label: 'Server', + host: 'example.com', + port: 22, + username: 'deploy' + } + const conn = {} + mockSshStore.getTarget.mockReturnValue(target) + mockConnectionManager.connect.mockResolvedValue(conn) + mockConnectionManager.getConnection.mockReturnValue(conn) + mockConnectionManager.getState.mockReturnValue({ + targetId: 'ssh-1', + status: 'connected', + error: null, + reconnectAttempt: 0 + }) + mockMux.probeLiveness.mockResolvedValue(false) + + await handlers.get('ssh:connect')!(null, { targetId: 'ssh-1' }) + + const resumeListener = powerMonitorOnMock.mock.calls.find(([event]) => event === 'resume')?.[1] + expect(resumeListener).toBeTypeOf('function') + + resumeListener() + // Why: the probe window is seconds long; a user disconnect during it must + // win — reconnecting afterwards would resurrect the torn-down target. + mockConnectionManager.getConnection.mockReturnValue(undefined) + + await vi.waitFor(() => expect(mockMux.probeLiveness).toHaveBeenCalledTimes(2)) + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(mockConnectionManager.reconnect).not.toHaveBeenCalled() + }) + it('extends active relay grace while the system is suspending', async () => { const target: SshTarget = { id: 'ssh-1', diff --git a/src/main/ipc/ssh.ts b/src/main/ipc/ssh.ts index 9f19406ca72..8b5756bff38 100644 --- a/src/main/ipc/ssh.ts +++ b/src/main/ipc/ssh.ts @@ -469,7 +469,8 @@ function registerPowerMonitorReconnect(): void { } const onResume = (): void => { for (const [targetId, session] of activeSessions) { - const conn = connectionManager?.getConnection(targetId) + const manager = connectionManager + const conn = manager?.getConnection(targetId) if (!conn) { continue } @@ -480,8 +481,14 @@ function registerPowerMonitorReconnect(): void { if (await isRelayLinkAliveAfterResume(session)) { return } + // Why: the probe can take ~10s. If the user disconnected or the + // session/connection was replaced meanwhile, reconnecting would + // resurrect a connection that was intentionally torn down. + if (activeSessions.get(targetId) !== session || manager?.getConnection(targetId) !== conn) { + return + } try { - await connectionManager?.reconnect(targetId) + await manager?.reconnect(targetId) } catch (err) { console.warn( `[ssh] Failed to reconnect ${targetId} after system resume: ${