fix: clean up system ssh probe listeners (#3824)

This commit is contained in:
Neil
2026-05-30 11:32:49 -07:00
committed by GitHub
parent a1a1928e20
commit 8044be7cc7
2 changed files with 86 additions and 38 deletions
+35
View File
@@ -565,6 +565,41 @@ describe('SshConnection', () => {
'printf ORCA-SYSTEM-SSH-OK'
)
})
it('removes system SSH probe listeners after timeout', async () => {
vi.useFakeTimers()
const channel = new EventEmitter() as ReturnType<typeof createSystemCommandChannel>
channel.stdin = { end: vi.fn(), write: vi.fn() }
channel.stderr = new EventEmitter()
channel.close = vi.fn()
spawnSystemSshCommandMock.mockReturnValueOnce(channel)
vi.mocked(resolveWithSshG).mockResolvedValueOnce({
hostname: 'example.com',
port: 22,
identityFile: [],
forwardAgent: false,
identitiesOnly: false,
proxyUseFdpass: true
})
const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks())
try {
const connect = expect(conn.connect()).rejects.toThrow('System SSH connection timed out')
await vi.advanceTimersByTimeAsync(30_000)
await connect
expect(channel.close).toHaveBeenCalled()
expect(channel.listenerCount('data')).toBe(0)
expect(channel.listenerCount('error')).toBe(1)
expect(channel.listenerCount('close')).toBe(1)
expect(channel.stderr.listenerCount('data')).toBe(0)
expect(
(conn as unknown as { systemCommandChannels: Set<unknown> }).systemCommandChannels.size
).toBe(0)
} finally {
vi.useRealTimers()
}
})
})
describe('shouldUseSystemSshTransport', () => {
+51 -38
View File
@@ -372,47 +372,60 @@ export class SshConnection {
let stdout = ''
let stderr = ''
let settled = false
const timeout = setTimeout(() => {
const cleanup = (): void => {
clearTimeout(timeout)
channel.off('data', onStdoutData)
channel.stderr.off('data', onStderrData)
channel.off('error', onError)
channel.off('close', onClose)
this.systemCommandChannels.delete(channel)
}
const settle = (callback: () => void): void => {
if (settled) {
return
}
settled = true
channel.close()
reject(new Error('System SSH connection timed out'))
cleanup()
callback()
}
const onStdoutData = (data: Buffer): void => {
stdout += data.toString('utf-8')
}
const onStderrData = (data: Buffer): void => {
stderr += data.toString('utf-8')
}
const onError = (err: Error): void => {
settle(() => reject(err))
}
const onClose = (code: number | null): void => {
settle(() => {
if (this.disposed || connectGeneration !== this.connectGeneration) {
reject(new Error('SSH connection attempt was cancelled'))
return
}
if (code !== 0 || !stdout.includes('ORCA-SYSTEM-SSH-OK')) {
reject(
new Error(
`System SSH probe failed${code != null ? ` (exit ${code})` : ''}.${stderr ? ` stderr: ${stderr.trim()}` : ''}`
)
)
return
}
this.setState('connected')
resolve()
})
}
const timeout = setTimeout(() => {
settle(() => {
channel.close()
reject(new Error('System SSH connection timed out'))
})
}, CONNECT_TIMEOUT_MS)
channel.on('data', (data: Buffer) => {
stdout += data.toString('utf-8')
})
channel.stderr.on('data', (data: Buffer) => {
stderr += data.toString('utf-8')
})
channel.on('error', (err: Error) => {
if (settled) {
return
}
settled = true
clearTimeout(timeout)
reject(err)
})
channel.on('close', (code: number | null) => {
if (settled) {
return
}
settled = true
clearTimeout(timeout)
if (this.disposed || connectGeneration !== this.connectGeneration) {
reject(new Error('SSH connection attempt was cancelled'))
return
}
if (code !== 0 || !stdout.includes('ORCA-SYSTEM-SSH-OK')) {
reject(
new Error(
`System SSH probe failed${code != null ? ` (exit ${code})` : ''}.${stderr ? ` stderr: ${stderr.trim()}` : ''}`
)
)
return
}
this.setState('connected')
resolve()
})
channel.on('data', onStdoutData)
channel.stderr.on('data', onStderrData)
channel.on('error', onError)
channel.on('close', onClose)
})
} catch (err) {
this.useSystemSshTransport = false