fix(wsl-cli): removing the WSL command from a deleted distro forgets its registration (#36227)

wsl.exe refuses any command for a distro that was unregistered, so Remove
failed and the distro stayed in wsl-cli-registrations.json for good. A
distro-not-found failure now drops every bookkeeping entry for it
(registration, inspection time, reconciliation) and reports not installed.

Fixes STA-12893
This commit is contained in:
Neil
2026-10-29 22:44:40 -07:00
committed by GitHub
parent 2b6e63ee2f
commit 11a066b934
5 changed files with 108 additions and 2 deletions
@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
forgetWslCliRegistration,
getWslCliRegistrationCandidates,
recordWslCliRegistrationObservations,
recordWslCliRegistrationRemoved
@@ -125,6 +126,29 @@ describe('WSL CLI registration registry', () => {
await expect(getWslCliRegistrationCandidates(userDataPath, ['Ubuntu'])).resolves.toEqual([])
})
it('forgets every entry of a deleted distro and leaves the others', async () => {
const reconciled = { target: 'C:\\Orca\\resources\\bin\\orca.exe', appVersion: '1.4.244' }
await recordWslCliRegistrationObservations(
userDataPath,
[
{ distro: 'orca-uqa-1029', inspected: true, managed: true, reconciled },
{ distro: 'Ubuntu', inspected: true, managed: true, reconciled }
],
{ now: 1_000 }
)
await forgetWslCliRegistration(userDataPath, 'ORCA-UQA-1029')
expect(
JSON.parse(await readFile(join(userDataPath, 'wsl-cli-registrations.json'), 'utf8'))
).toEqual({
schemaVersion: 2,
registeredDistros: ['Ubuntu'],
inspectionTimes: { ubuntu: 1_000 },
reconciliations: { ubuntu: reconciled }
})
})
it('periodically re-inspects a negative entry so restored distros are discovered', async () => {
await recordWslCliRegistrationObservations(
userDataPath,
@@ -270,3 +270,18 @@ export function recordWslCliRegistrationRemoved(
{ distro, inspected: true, managed: false, reconciled: null }
])
}
/** A deleted distro has nothing left to inspect or reconcile, so every entry for it goes. */
export function forgetWslCliRegistration(userDataPath: string, distro: string): Promise<void> {
const key = normalizeWslDistroKey(distro)
return updateState(userDataPath, (state) => {
const { [key]: _inspected, ...inspectionTimes } = state.inspectionTimes
const { [key]: _reconciled, ...reconciliations } = state.reconciliations
return {
...state,
registeredDistros: removeDistro(state.registeredDistros, distro),
inspectionTimes,
reconciliations
}
})
}
+18
View File
@@ -119,6 +119,24 @@ export function unsupportedWslCliStatus(
}
}
/** The distro was deleted (`wsl --unregister`), so nothing of Orca's is left in it. */
export function deletedDistroWslCliStatus(distro: string): CliInstallStatus {
return {
platform: 'linux',
commandName: WSL_COMMAND_NAME,
commandPath: null,
pathDirectory: null,
pathConfigured: false,
launcherPath: null,
installMethod: null,
supported: false,
state: 'not_installed',
currentTarget: null,
unsupportedReason: null,
detail: `${distro} is no longer an installed WSL distro, so nothing is registered in it.`
}
}
/** Settings shows this instead of a raw IPC timeout; the row stays refreshable. */
export function unreachableWslCliStatus(distro: string): CliInstallStatus {
return unsupportedWslCliStatus(
+34 -1
View File
@@ -7,6 +7,7 @@ const {
wslInstallerMock,
recordInstalledMock,
recordRemovedMock,
forgetMock,
getDefaultWslDistroMock,
readWslDistroRunningStateMock
} = vi.hoisted(() => ({
@@ -16,6 +17,7 @@ const {
wslInstallerMock: vi.fn(),
recordInstalledMock: vi.fn(),
recordRemovedMock: vi.fn(),
forgetMock: vi.fn(),
getDefaultWslDistroMock: vi.fn()
}))
@@ -24,7 +26,8 @@ vi.mock('../cli/cli-installer', () => ({ CliInstaller: vi.fn() }))
vi.mock('../cli/wsl-cli-installer', () => ({ WslCliInstaller: wslInstallerMock }))
vi.mock('../cli/wsl-cli-registration-registry', () => ({
recordWslCliRegistrationInstalled: recordInstalledMock,
recordWslCliRegistrationRemoved: recordRemovedMock
recordWslCliRegistrationRemoved: recordRemovedMock,
forgetWslCliRegistration: forgetMock
}))
vi.mock('../persistence', () => ({ getCanonicalUserDataPath: () => '/canonical-user-data' }))
vi.mock('../startup/hydrate-shell-path', () => ({
@@ -65,6 +68,7 @@ describe('WSL CLI registration IPC', () => {
wslInstallerMock.mockReset()
recordInstalledMock.mockReset().mockResolvedValue(undefined)
recordRemovedMock.mockReset().mockResolvedValue(undefined)
forgetMock.mockReset().mockResolvedValue(undefined)
getDefaultWslDistroMock.mockReset().mockReturnValue('Ubuntu')
registerCliHandlers()
})
@@ -120,6 +124,35 @@ describe('WSL CLI registration IPC', () => {
expect(recordRemovedMock).toHaveBeenCalledWith('/canonical-user-data', 'Ubuntu')
})
it('forgets the registration of a deleted distro instead of failing the removal', async () => {
const remove = vi.fn(async () => {
throw new Error('wsl.exe: There is no distribution with the supplied name.')
})
wslInstallerMock.mockImplementation(function MockWslCliInstaller() {
return { remove }
})
await expect(
getWslHandler('cli:removeWsl')({}, { distro: 'orca-uqa-1029' })
).resolves.toMatchObject({ state: 'not_installed', supported: false })
expect(forgetMock).toHaveBeenCalledWith('/canonical-user-data', 'orca-uqa-1029')
expect(recordRemovedMock).not.toHaveBeenCalled()
})
it('keeps the registration when removal fails for another reason', async () => {
const remove = vi.fn(async () => {
throw new Error('permission denied')
})
wslInstallerMock.mockImplementation(function MockWslCliInstaller() {
return { remove }
})
await expect(getWslHandler('cli:removeWsl')({}, { distro: 'Ubuntu' })).rejects.toThrow(
'permission denied'
)
expect(forgetMock).not.toHaveBeenCalled()
})
it('does not claim ownership when installation is not confirmed', async () => {
const install = vi.fn(async () => ({ state: 'unsupported' }))
wslInstallerMock.mockImplementation(function MockWslCliInstaller() {
+17 -1
View File
@@ -4,11 +4,13 @@ import { resolveAppImageCacheKey } from '../cli/appimage-extracted-root'
import { CliInstaller } from '../cli/cli-installer'
import { runKeyedSerializedOperation } from '../cli/keyed-promise-queue'
import {
forgetWslCliRegistration,
recordWslCliRegistrationInstalled,
recordWslCliRegistrationRemoved
} from '../cli/wsl-cli-registration-registry'
import { WslCliInstaller } from '../cli/wsl-cli-installer'
import {
deletedDistroWslCliStatus,
notRunningWslCliStatus,
unreachableWslCliStatus,
WslCliUnreachableError
@@ -18,6 +20,7 @@ import { getCanonicalUserDataPath } from '../persistence'
import { hydrateShellPath, mergePathSegments } from '../startup/hydrate-shell-path'
import { getDefaultWslDistro } from '../wsl'
import { blockedWslDistroForExec, runAsWslExecUserAct } from '../wsl/wsl-background-exec-gate'
import { isWslDistroNotFoundFailure } from '../wsl/wsl-distro-unsupported'
import { resolveAppImageRuntimeIdentity } from '../appimage-runtime-identity'
const APPIMAGE_REPAIR_RETRY_MS = 30_000
@@ -189,7 +192,20 @@ export function registerCliHandlers(): void {
async (_event, args?: { distro?: string | null }): Promise<CliInstallStatus> => {
const distro = resolveWslCliDistro(args)
return runWslCliRegistrationOperation(distro, async () => {
const status = await new WslCliInstaller({ distro }).remove()
let status: CliInstallStatus
try {
status = await new WslCliInstaller({ distro }).remove()
} catch (error) {
if (!distro || !isWslDistroNotFoundFailure(error)) {
throw error
}
// Nothing is left inside a deleted distro; only Orca's bookkeeping still names it.
await persistWslCliRegistration(
() => forgetWslCliRegistration(getCanonicalUserDataPath(), distro),
'remove'
)
return deletedDistroWslCliStatus(distro)
}
if (distro && status.state === 'not_installed') {
await persistWslCliRegistration(
() => recordWslCliRegistrationRemoved(getCanonicalUserDataPath(), distro),