fix: recognize working WSL1 without a WSL2 kernel

This commit is contained in:
Neil
2026-09-06 02:44:08 -07:00
parent 6933fd70d7
commit b33817a03b
3 changed files with 136 additions and 1 deletions
+14 -1
View File
@@ -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<boolean> {
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
})
+85
View File
@@ -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<typeof execFile>
})
}
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()
})
})
}
+37
View File
@@ -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<boolean> {
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
}
}