From 82288ea3ebea366b47e3c21b7355493f43e1bdea Mon Sep 17 00:00:00 2001 From: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:36:54 -0700 Subject: [PATCH] fix(windows): drop a stale WSL availability failure once a distro list succeeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/main/wsl.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++ src/main/wsl.ts | 14 ++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/main/wsl.test.ts b/src/main/wsl.test.ts index 855f7212219..b37a4e1e0ae 100644 --- a/src/main/wsl.test.ts +++ b/src/main/wsl.test.ts @@ -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) diff --git a/src/main/wsl.ts b/src/main/wsl.ts index 54b259cc4ea..2d2bbcb9fd5 100644 --- a/src/main/wsl.ts +++ b/src/main/wsl.ts @@ -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