fix(windows): drop a stale WSL availability failure once a distro list succeeds

The distro-list and availability caches expire independently, and
getWslRepairReason checks availability first. So a definitive availability
failure (numeric exit status or ENOENT) held for 10-30min would keep reporting
`wsl-unavailable` even after `wsl --list --quiet` successfully returned a
distro — i.e. over a WSL that demonstrably just answered. That is the same
latch class this branch fixes, surviving in the gap between the two caches.

A non-empty distro list proves wsl.exe ran, so drop the negative availability
cache and let the next call re-probe. Scoped to non-empty lists only: those are
cached for the process lifetime, so this cannot re-spawn the blocking 5s probe
more than once. An empty list keeps its failure cache, since it re-probes on a
15s-to-5min schedule and would otherwise pay the blocking probe far too often.
This commit is contained in:
OrcaWin
2026-07-29 17:36:54 -07:00
parent 3f90d2cf91
commit 82288ea3eb
2 changed files with 67 additions and 0 deletions
+53
View File
@@ -532,6 +532,59 @@ describe('WSL availability cache', () => {
}
})
// Why: the caches expire independently and `getWslRepairReason` checks availability
// first, so a definitive failure held for 10min would report `wsl-unavailable` over a
// WSL that just listed a distro. Finding a distro must drop the stale failure.
it.each([
['a definitive failure', { status: 1 }],
['a timeout', { code: 'ETIMEDOUT', status: null, signal: 'SIGTERM' }]
])('re-probes availability once a distro list succeeds after %s', (_label, errorShape) => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw Object.assign(new Error('probe failed'), errorShape)
})
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
// A distro turns up (WSL finished provisioning / was repaired mid-session).
execFileSyncMock.mockReturnValueOnce('Ubuntu\n')
expect(listWslDistros()).toEqual(['Ubuntu'])
// Without dropping the stale failure this would stay false for 10min.
execFileSyncMock.mockReturnValueOnce('')
expect(isWslAvailable()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: an empty list re-probes on a 15s-to-5min schedule, so clearing the availability
// failure on every empty probe would re-spawn the blocking 5s probe far too often.
it('does not drop an availability failure for an empty distro list', () => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw Object.assign(new Error('probe failed'), { status: 1 })
})
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
execFileSyncMock.mockReturnValueOnce('')
expect(listWslDistros()).toEqual([])
expect(getCachedWslAvailability()).toBe(false)
// Still inside the definitive window, so no re-probe was paid.
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
expect(isWslAvailable()).toBe(false)
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
})
} finally {
vi.useRealTimers()
}
})
it('keeps reporting unavailable off Windows without probing', () => {
withPlatform('darwin', () => {
expect(isWslAvailable()).toBe(false)
+14
View File
@@ -137,6 +137,9 @@ function cacheWslDistroList(rawDistros: string[]): string[] {
if (userDistros.length === 0 && wslDistroCache !== null && wslDistroCache.length > 0) {
return wslDistroCache
}
if (userDistros.length > 0) {
dropStaleWslAvailabilityFailure()
}
wslDistroCache = userDistros
if (wslDistroCache.length === 0) {
const now = Date.now()
@@ -324,6 +327,17 @@ function isRetryableWslProbeFailure(error: unknown): boolean {
return failure?.code !== 'ENOENT'
}
// Why: the two caches expire independently, and `getWslRepairReason` checks availability
// first — so a definitive failure held for 10-30min would report `wsl-unavailable` over a
// WSL that just listed a distro for us. A non-empty list proves wsl.exe ran, so drop the
// stale failure and let the next call re-probe. Non-empty lists are cached for the process
// lifetime, so this cannot re-spawn the blocking probe more than once.
function dropStaleWslAvailabilityFailure(): void {
if (wslAvailableCache && !wslAvailableCache.available && !('unsupported' in wslAvailableCache)) {
wslAvailableCache = null
}
}
function isWslAvailabilityCacheFresh(cache: WslAvailabilityCache): boolean {
if (isPermanentWslAvailabilityCache(cache)) {
return true