diff --git a/src/main/cli/wsl-cli-registration-registry.test.ts b/src/main/cli/wsl-cli-registration-registry.test.ts index fb91741d738..4eec3a27a08 100644 --- a/src/main/cli/wsl-cli-registration-registry.test.ts +++ b/src/main/cli/wsl-cli-registration-registry.test.ts @@ -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, diff --git a/src/main/cli/wsl-cli-registration-registry.ts b/src/main/cli/wsl-cli-registration-registry.ts index 002e6e60143..e4b2f7da833 100644 --- a/src/main/cli/wsl-cli-registration-registry.ts +++ b/src/main/cli/wsl-cli-registration-registry.ts @@ -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 { + 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 + } + }) +} diff --git a/src/main/cli/wsl-cli-status.ts b/src/main/cli/wsl-cli-status.ts index 0557b250e0b..fe38c2d2349 100644 --- a/src/main/cli/wsl-cli-status.ts +++ b/src/main/cli/wsl-cli-status.ts @@ -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( diff --git a/src/main/ipc/cli.test.ts b/src/main/ipc/cli.test.ts index 29060ab8004..c75190215f6 100644 --- a/src/main/ipc/cli.test.ts +++ b/src/main/ipc/cli.test.ts @@ -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() { diff --git a/src/main/ipc/cli.ts b/src/main/ipc/cli.ts index b56486bd53d..ccb9cdbbbb0 100644 --- a/src/main/ipc/cli.ts +++ b/src/main/ipc/cli.ts @@ -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 => { 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),