From b33817a03b10adc03398cd4e7fda68913b032df2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:44:08 -0700 Subject: [PATCH] fix: recognize working WSL1 without a WSL2 kernel --- src/main/wsl-availability.ts | 15 +++- src/main/wsl-missing-kernel-probe.test.ts | 85 +++++++++++++++++++++++ src/main/wsl-missing-kernel-probe.ts | 37 ++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/main/wsl-missing-kernel-probe.test.ts create mode 100644 src/main/wsl-missing-kernel-probe.ts diff --git a/src/main/wsl-availability.ts b/src/main/wsl-availability.ts index 1d14f526413..f8505cf6b84 100644 --- a/src/main/wsl-availability.ts +++ b/src/main/wsl-availability.ts @@ -1,5 +1,10 @@ import { execFile, execFileSync } from 'node:child_process' import { resolveWslInteropSpawnCwd } from './wsl-interop-spawn-directory' +import { + canExecuteWslWithoutKernel, + canExecuteWslWithoutKernelSync, + isWslMissingKernelError +} from './wsl-missing-kernel-probe' type WslAvailabilityCache = | { available: true } @@ -147,6 +152,9 @@ export function isWslAvailable(): boolean { }) return cacheWslAvailabilityProbeResult(null, startedAtGeneration) } catch (error) { + if (isWslMissingKernelError(error) && canExecuteWslWithoutKernelSync()) { + return cacheWslAvailabilityProbeResult(null, startedAtGeneration) + } return cacheWslAvailabilityProbeResult(error, startedAtGeneration) } } @@ -176,7 +184,12 @@ export function isWslAvailableAsync(): Promise { const startedAtGeneration = wslAvailabilityCacheGeneration wslAvailabilityProbeInFlight = probeWslStatus() .then(() => cacheWslAvailabilityProbeResult(null, startedAtGeneration)) - .catch((error: unknown) => cacheWslAvailabilityProbeResult(error, startedAtGeneration)) + .catch(async (error: unknown) => { + if (isWslMissingKernelError(error) && (await canExecuteWslWithoutKernel())) { + return cacheWslAvailabilityProbeResult(null, startedAtGeneration) + } + return cacheWslAvailabilityProbeResult(error, startedAtGeneration) + }) .finally(() => { wslAvailabilityProbeInFlight = null }) diff --git a/src/main/wsl-missing-kernel-probe.test.ts b/src/main/wsl-missing-kernel-probe.test.ts new file mode 100644 index 00000000000..d6bf8c0732c --- /dev/null +++ b/src/main/wsl-missing-kernel-probe.test.ts @@ -0,0 +1,85 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { execFile, execFileSync } from 'node:child_process' +import { runProcess, runProcessSync } from '../shared/child-process/run-process' +import { + _resetWslAvailabilityCacheForTests, + isWslAvailable, + isWslAvailableAsync +} from './wsl-availability' + +vi.mock('node:child_process', () => ({ execFile: vi.fn(), execFileSync: vi.fn() })) +vi.mock('../shared/child-process/run-process', () => ({ + runProcess: vi.fn(), + runProcessSync: vi.fn() +})) +vi.mock('./wsl-interop-spawn-directory', () => ({ + resolveWslInteropSpawnCwd: () => 'C:\\Windows' +})) + +const originalPlatform = process.platform +const success = { code: 0, signal: null, stdout: '', stderr: '', timedOut: false } + +beforeEach(() => { + vi.resetAllMocks() + Object.defineProperty(process, 'platform', { value: 'win32' }) + _resetWslAvailabilityCacheForTests() +}) +afterEach(() => { + Object.defineProperty(process, 'platform', { value: originalPlatform }) + _resetWslAvailabilityCacheForTests() +}) + +for (const mode of ['sync', 'async'] as const) { + describe(`${mode} WSL1 availability without WSL2 kernel`, () => { + const probe = () => (mode === 'sync' ? isWslAvailable() : isWslAvailableAsync()) + function failStatus(code: number): void { + vi.mocked(execFileSync).mockImplementation(() => { + throw { status: code } + }) + vi.mocked(execFile).mockImplementation((...args: unknown[]) => { + const callback = args.at(-1) as (error: unknown) => void + callback({ code }) + return {} as ReturnType + }) + } + function guestResult(result: typeof success): void { + vi.mocked(runProcess).mockResolvedValue(result) + vi.mocked(runProcessSync).mockReturnValue(result) + } + + it('requires guest execution and caches its success', async () => { + failStatus(-444) + guestResult(success) + expect(await probe()).toBe(true) + expect(await probe()).toBe(true) + const runner = mode === 'sync' ? runProcessSync : runProcess + expect(runner).toHaveBeenCalledTimes(1) + expect(runner).toHaveBeenCalledWith( + expect.objectContaining({ + program: 'wsl.exe', + args: ['--exec', '/bin/true'], + timeoutMs: 5000, + cwd: 'C:\\Windows' + }) + ) + }) + + for (const result of [ + { ...success, code: 1 }, + { ...success, timedOut: true } + ]) { + it(`keeps failed guest unavailable: ${JSON.stringify(result)}`, async () => { + failStatus(-444) + guestResult(result) + expect(await probe()).toBe(false) + }) + } + + it('does not probe a guest for unrelated status failures', async () => { + failStatus(1) + expect(await probe()).toBe(false) + expect(runProcess).not.toHaveBeenCalled() + expect(runProcessSync).not.toHaveBeenCalled() + }) + }) +} diff --git a/src/main/wsl-missing-kernel-probe.ts b/src/main/wsl-missing-kernel-probe.ts new file mode 100644 index 00000000000..d196c3321a2 --- /dev/null +++ b/src/main/wsl-missing-kernel-probe.ts @@ -0,0 +1,37 @@ +import { runProcess, runProcessSync, type ProcessSpec } from '../shared/child-process/run-process' +import { buildWslExecArgs } from '../shared/wsl-login-shell-command' +import { resolveWslInteropSpawnCwd } from './wsl-interop-spawn-directory' + +export function isWslMissingKernelError(error: unknown): boolean { + const failure = error as { code?: unknown; status?: unknown } | null + return failure?.code === -444 || failure?.status === -444 +} + +function defaultGuestProbe(): ProcessSpec { + return { + program: 'wsl.exe', + args: buildWslExecArgs(undefined, ['/bin/true']), + cwd: resolveWslInteropSpawnCwd(), + timeoutMs: 5000, + maxOutputBytes: 4096 + } +} + +// WSL1 can execute normally while --status rejects a missing WSL2 kernel. +export async function canExecuteWslWithoutKernel(): Promise { + try { + const result = await runProcess(defaultGuestProbe()) + return result.code === 0 && !result.timedOut + } catch { + return false + } +} + +export function canExecuteWslWithoutKernelSync(): boolean { + try { + const result = runProcessSync(defaultGuestProbe()) + return result.code === 0 && !result.timedOut + } catch { + return false + } +}