mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
fix: time out ssh directory browsing (#3794)
This commit is contained in:
@@ -108,4 +108,41 @@ describe('registerSshBrowseHandler', () => {
|
||||
expect(channel.stderr.listenerCount('data')).toBe(0)
|
||||
expect(channel.stderr.listenerCount('error')).toBe(0)
|
||||
})
|
||||
|
||||
it('times out browse channels that never close', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const channel = createMockChannel()
|
||||
const exec = vi.fn().mockResolvedValue(channel)
|
||||
const getConnectionManager = () => ({
|
||||
getConnection: () => ({ exec })
|
||||
})
|
||||
registerSshBrowseHandler(getConnectionManager as never)
|
||||
|
||||
const resultPromise = handler(null, { targetId: 'ssh-1', dirPath: '/mnt/stalled' })
|
||||
let settled = false
|
||||
void resultPromise.then(
|
||||
() => {
|
||||
settled = true
|
||||
},
|
||||
() => {
|
||||
settled = true
|
||||
}
|
||||
)
|
||||
|
||||
await Promise.resolve()
|
||||
await vi.advanceTimersByTimeAsync(15_000)
|
||||
|
||||
expect(settled).toBe(true)
|
||||
await expect(resultPromise).rejects.toThrow('Remote directory listing timed out')
|
||||
expect(channel.listenerCount('data')).toBe(0)
|
||||
expect(channel.listenerCount('exit')).toBe(0)
|
||||
expect(channel.listenerCount('close')).toBe(0)
|
||||
expect(channel.listenerCount('error')).toBe(0)
|
||||
expect(channel.stderr.listenerCount('data')).toBe(0)
|
||||
expect(channel.stderr.listenerCount('error')).toBe(0)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,8 @@ export type RemoteDirEntry = {
|
||||
isDirectory: boolean
|
||||
}
|
||||
|
||||
const SSH_BROWSE_TIMEOUT_MS = 15_000
|
||||
|
||||
// Why: the relay's fs.readDir enforces workspace root ACLs, which aren't
|
||||
// registered until a repo is added. This handler uses a raw SSH exec channel
|
||||
// to list directories, allowing the user to browse the remote filesystem
|
||||
@@ -45,8 +47,13 @@ export function registerSshBrowseHandler(
|
||||
let stderr = ''
|
||||
let exitCode: number | null = null
|
||||
let settled = false
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const cleanup = (): void => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
channel.off('data', onStdoutData)
|
||||
channel.stderr.off('data', onStderrData)
|
||||
channel.off('exit', onExit)
|
||||
@@ -62,6 +69,25 @@ export function registerSshBrowseHandler(
|
||||
cleanup()
|
||||
reject(error)
|
||||
}
|
||||
const closeChannel = (): void => {
|
||||
const closable = channel as { close?: () => void; destroy?: () => void }
|
||||
try {
|
||||
if (typeof closable.close === 'function') {
|
||||
closable.close()
|
||||
} else if (typeof closable.destroy === 'function') {
|
||||
closable.destroy()
|
||||
}
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
}
|
||||
const onTimeout = (): void => {
|
||||
// Why: remote browsing runs before a relay workspace root exists, so
|
||||
// it cannot rely on relay request deadlines. Bound this raw exec
|
||||
// channel directly to keep Add Remote Project from hanging forever.
|
||||
rejectOnce(new Error('Remote directory listing timed out'))
|
||||
closeChannel()
|
||||
}
|
||||
const resolveOnce = (result: { entries: RemoteDirEntry[]; resolvedPath: string }): void => {
|
||||
if (settled) {
|
||||
return
|
||||
@@ -147,6 +173,10 @@ export function registerSshBrowseHandler(
|
||||
// scoped listener, a disappearing remote can become process-fatal.
|
||||
channel.on('error', onError)
|
||||
channel.stderr.on('error', onError)
|
||||
timeout = setTimeout(onTimeout, SSH_BROWSE_TIMEOUT_MS)
|
||||
if (typeof timeout.unref === 'function') {
|
||||
timeout.unref()
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user