fix: time out local preflight probes (#3791)

This commit is contained in:
Neil
2026-05-30 10:29:54 -07:00
committed by GitHub
parent efc7d80044
commit ee96d4a048
2 changed files with 87 additions and 9 deletions
+45 -2
View File
@@ -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,
+42 -7
View File
@@ -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 ?? ''