mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 16:02:41 +00:00
fix: time out local preflight probes (#3791)
This commit is contained in:
@@ -145,10 +145,12 @@ describe('preflight', () => {
|
||||
gitea: defaultGiteaStatus
|
||||
})
|
||||
expect(execFileAsyncMock).toHaveBeenNthCalledWith(4, 'gh', ['auth', 'status'], {
|
||||
encoding: 'utf-8'
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000
|
||||
})
|
||||
expect(execFileAsyncMock).toHaveBeenNthCalledWith(5, 'glab', ['auth', 'status'], {
|
||||
encoding: 'utf-8'
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000
|
||||
})
|
||||
})
|
||||
|
||||
@@ -206,6 +208,47 @@ describe('preflight', () => {
|
||||
expect(status.glab).toEqual({ installed: true, authenticated: false })
|
||||
})
|
||||
|
||||
it('times out hung local preflight probes', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
execFileAsyncMock.mockImplementation((command, args) => {
|
||||
if (command === 'git') {
|
||||
return Promise.resolve({ stdout: 'git version 2.0.0\n' })
|
||||
}
|
||||
if (command === 'gh' && Array.isArray(args) && args[0] === '--version') {
|
||||
return new Promise(() => {})
|
||||
}
|
||||
if (command === 'glab') {
|
||||
return Promise.reject(new Error('command not found: glab'))
|
||||
}
|
||||
throw new Error(`unexpected command ${String(command)}`)
|
||||
})
|
||||
|
||||
const statusPromise = runPreflightCheck()
|
||||
let settled = false
|
||||
void statusPromise.then(
|
||||
() => {
|
||||
settled = true
|
||||
},
|
||||
() => {
|
||||
settled = true
|
||||
}
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(settled).toBe(true)
|
||||
await expect(statusPromise).resolves.toMatchObject({
|
||||
git: { installed: true },
|
||||
gh: { installed: false },
|
||||
glab: { installed: false }
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('prefers the selected WSL distro when checking gh for a WSL workspace', async () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getGiteaAuthStatus } from '../gitea/client'
|
||||
import { _resetKnownHostsCache } from '../gitlab/gl-utils'
|
||||
import { getActiveMultiplexer } from './ssh'
|
||||
const execFileAsync = promisify(execFile)
|
||||
const PREFLIGHT_COMMAND_TIMEOUT_MS = 5000
|
||||
|
||||
type PreflightRuntimeContext = {
|
||||
wslDistro?: string | null
|
||||
@@ -59,6 +60,42 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, "'\\''")}'`
|
||||
}
|
||||
|
||||
type PreflightCommandResult = { stdout: string; stderr: string }
|
||||
|
||||
// Why: a broken PATH shim or auth helper should not keep startup/settings
|
||||
// preflight IPC pending forever; WSL probes already use the same deadline.
|
||||
async function execLocalPreflightCommand(
|
||||
command: string,
|
||||
args: string[]
|
||||
): Promise<PreflightCommandResult> {
|
||||
const commandPromise = execFileAsync(command, args, {
|
||||
encoding: 'utf-8',
|
||||
timeout: PREFLIGHT_COMMAND_TIMEOUT_MS
|
||||
}) as Promise<PreflightCommandResult>
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
try {
|
||||
return await Promise.race([
|
||||
commandPromise,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
const error = Object.assign(new Error(`Timed out running ${command}`), {
|
||||
code: 'ETIMEDOUT'
|
||||
})
|
||||
reject(error)
|
||||
}, PREFLIGHT_COMMAND_TIMEOUT_MS)
|
||||
if (typeof timeout.unref === 'function') {
|
||||
timeout.unref()
|
||||
}
|
||||
})
|
||||
])
|
||||
} finally {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function execCommandInWsl(
|
||||
target: WslPreflightTarget,
|
||||
command: string
|
||||
@@ -66,7 +103,7 @@ async function execCommandInWsl(
|
||||
const distroArgs = target.distro ? ['-d', target.distro] : []
|
||||
return execFileAsync('wsl.exe', [...distroArgs, '--', 'bash', '-lc', command], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000
|
||||
timeout: PREFLIGHT_COMMAND_TIMEOUT_MS
|
||||
}) as Promise<{ stdout: string; stderr: string }>
|
||||
}
|
||||
|
||||
@@ -77,7 +114,7 @@ async function isCommandAvailable(
|
||||
try {
|
||||
await (wslTarget
|
||||
? execCommandInWsl(wslTarget, `${shellQuote(command)} --version`)
|
||||
: execFileAsync(command, ['--version']))
|
||||
: execLocalPreflightCommand(command, ['--version']))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
@@ -92,7 +129,7 @@ async function isCommandOnPath(command: string, wslTarget?: WslPreflightTarget):
|
||||
try {
|
||||
const { stdout } = wslTarget
|
||||
? await execCommandInWsl(wslTarget, `command -v ${shellQuote(command)}`)
|
||||
: await execFileAsync(finder, [command], { encoding: 'utf-8' })
|
||||
: await execLocalPreflightCommand(finder, [command])
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
@@ -198,9 +235,7 @@ async function isGhAuthenticated(wslTarget?: WslPreflightTarget): Promise<boolea
|
||||
try {
|
||||
await (wslTarget
|
||||
? execCommandInWsl(wslTarget, `${shellQuote('gh')} auth status`)
|
||||
: execFileAsync('gh', ['auth', 'status'], {
|
||||
encoding: 'utf-8'
|
||||
}))
|
||||
: execLocalPreflightCommand('gh', ['auth', 'status']))
|
||||
// Why: for plain-text `gh auth status`, exit 0 means gh did not detect any
|
||||
// authentication issues for the checked hosts/accounts.
|
||||
return true
|
||||
@@ -221,7 +256,7 @@ async function isGlabAuthenticated(wslTarget?: WslPreflightTarget): Promise<bool
|
||||
try {
|
||||
await (wslTarget
|
||||
? execCommandInWsl(wslTarget, `${shellQuote('glab')} auth status`)
|
||||
: execFileAsync('glab', ['auth', 'status'], { encoding: 'utf-8' }))
|
||||
: execLocalPreflightCommand('glab', ['auth', 'status']))
|
||||
return true
|
||||
} catch (error) {
|
||||
const stdout = (error as { stdout?: string }).stdout ?? ''
|
||||
|
||||
Reference in New Issue
Block a user