fix: time out relay rg availability probes (#3779)

This commit is contained in:
Neil 2026-05-30 09:55:51 -07:00 committed by GitHub
parent e5fcb2e2d1
commit d05c3ada2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 9 deletions

View File

@ -0,0 +1,50 @@
import { EventEmitter } from 'events'
import { describe, expect, it, vi } from 'vitest'
const { execFileMock, spawnMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('child_process', () => ({
execFile: execFileMock,
spawn: spawnMock
}))
import { checkRgAvailable } from './fs-handler-utils'
class FakeChildProcess extends EventEmitter {
kill = vi.fn()
}
describe('relay rg availability', () => {
it('removes listeners after a successful probe', async () => {
const child = new FakeChildProcess()
execFileMock.mockReturnValueOnce(child)
const result = checkRgAvailable()
child.emit('close', 0)
await expect(result).resolves.toBe(true)
expect(child.listenerCount('error')).toBe(0)
expect(child.listenerCount('close')).toBe(0)
})
it('settles and detaches when a wedged probe ignores timeout kill', async () => {
vi.useFakeTimers()
try {
const child = new FakeChildProcess()
execFileMock.mockReturnValueOnce(child)
const result = checkRgAvailable()
await vi.advanceTimersByTimeAsync(5000)
await expect(result).resolves.toBe(false)
expect(child.kill).toHaveBeenCalledTimes(1)
expect(child.listenerCount('error')).toBe(0)
expect(child.listenerCount('close')).toBe(0)
} finally {
vi.useRealTimers()
}
})
})

View File

@ -186,24 +186,41 @@ export function searchWithRg(
// was uninstalled or broken mid-session. The `settled` flag below closes
// the original race between 'error' and 'close' that the cache was added
// to paper over, so re-checking per call is both simpler and safer.
const RG_AVAILABILITY_TIMEOUT_MS = 5000
export function checkRgAvailable(): Promise<boolean> {
return new Promise((resolve) => {
let settled = false
const child = execFile('rg', ['--version'])
child.once('error', () => {
let timeout: ReturnType<typeof setTimeout> | null = null
const cleanup = (): void => {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
child.off('error', onError)
child.off('close', onClose)
}
const settle = (available: boolean, options?: { kill?: boolean }): void => {
if (settled) {
return
}
settled = true
resolve(false)
})
child.once('close', (code) => {
if (settled) {
return
cleanup()
if (options?.kill) {
child.kill()
}
settled = true
resolve(code === 0)
})
resolve(available)
}
const onError = (): void => settle(false)
const onClose = (code: number | null): void => settle(code === 0)
child.once('error', onError)
child.once('close', onClose)
timeout = setTimeout(() => settle(false, { kill: true }), RG_AVAILABILITY_TIMEOUT_MS)
if (typeof timeout.unref === 'function') {
timeout.unref()
}
})
}