fix: time out relay rg availability probes (#3779)
This commit is contained in:
parent
e5fcb2e2d1
commit
d05c3ada2d
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue