From aa05edb8d6dd19752884afdfda12c35d90ae667d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:35:11 -0700 Subject: [PATCH] fix(wsl-host): a failed removal no longer leaves the host stuck as "being removed" (#29921) remove() marked the host as being removed, then awaited disconnect and releaseHolder with no cleanup on failure. A throw (EBUSY/EPERM on Windows) left every later connect and runtime call refused. Clear the mark in a finally, keep the record and its visible work (dropped only on success), and rethrow so the user sees the error and can retry. --- src/main/wsl/wsl-host-service.test.ts | 20 ++++++++++++++++++++ src/main/wsl/wsl-host-service.ts | 27 ++++++++++++++++----------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/main/wsl/wsl-host-service.test.ts b/src/main/wsl/wsl-host-service.test.ts index 7df9c2b9e6c..c8ca64eb150 100644 --- a/src/main/wsl/wsl-host-service.test.ts +++ b/src/main/wsl/wsl-host-service.test.ts @@ -352,6 +352,26 @@ describe('WslHostService', () => { expect(listEnvironments(userDataPath)).toEqual([]) }) + it('keeps a host whose removal failed usable and removable again', async () => { + const connector = makeConnector([UBUNTU]) + const service = makeService(connector) + await service.add({ distro: 'Ubuntu' }) + service.setVisibleWork(['wsl:ubuntu']) + connector.releaseHolder.mockRejectedValueOnce( + Object.assign(new Error('EBUSY: resource busy or locked'), { code: 'EBUSY' }) + ) + + await expect(service.remove('wsl:ubuntu')).rejects.toThrow('EBUSY') + + expect(listEnvironments(userDataPath)).toHaveLength(1) + expect(removed).not.toHaveBeenCalled() + await expect(service.ensureForRuntimeCall('wsl:ubuntu')).resolves.toBeUndefined() + await expect(service.connect('wsl:ubuntu')).resolves.toBeUndefined() + await expect(service.remove('wsl:ubuntu')).resolves.toEqual({ stopped: true }) + expect(listEnvironments(userDataPath)).toEqual([]) + expect(removed).toHaveBeenCalledWith('wsl:ubuntu') + }) + it('turning the experiment off closes every connection and releases holders; on resumes lazily', async () => { const connector = makeConnector([UBUNTU]) const service = makeService(connector) diff --git a/src/main/wsl/wsl-host-service.ts b/src/main/wsl/wsl-host-service.ts index 49832a52f9d..b2a3625275f 100644 --- a/src/main/wsl/wsl-host-service.ts +++ b/src/main/wsl/wsl-host-service.ts @@ -139,19 +139,23 @@ export class WslHostService { } // First: nothing may reconnect it while it is being stopped (open work, liveness, calls). this.removing.add(environmentId) - this.visible.delete(environmentId) const connector = this.connector let serverStop: WslHostServerStop = { stopped: true } - if (connector) { - serverStop = await connector - .stopServer(record.distro, environmentId) - .catch((error: unknown) => ({ stopped: false, reason: errorMessage(error) })) - await connector.disconnect(record.distro) - // Without its holder the distro idles out. - await connector.releaseHolder(record.distro) + try { + if (connector) { + serverStop = await connector + .stopServer(record.distro, environmentId) + .catch((error: unknown) => ({ stopped: false, reason: errorMessage(error) })) + await connector.disconnect(record.distro) + // Without its holder the distro idles out. + await connector.releaseHolder(record.distro) + } + removeWslHostEnvironment(this.deps.userDataPath(), environmentId) + this.visible.delete(environmentId) + } finally { + // Why: a failed removal (e.g. EBUSY on Windows) must leave the host usable and removable. + this.removing.delete(environmentId) } - removeWslHostEnvironment(this.deps.userDataPath(), environmentId) - this.removing.delete(environmentId) this.statuses.delete(environmentId) await this.deps.removed(environmentId) return serverStop @@ -236,7 +240,8 @@ export class WslHostService { hasUserVisibleWork(distro: string): boolean { const record = this.recordForDistro(distro) // Why the gate: liveness relaunches only for visible work, and none may run while off. - return record ? this.canRun() && this.visible.has(record.environmentId) : false + const id = record?.environmentId + return id ? this.canRun() && this.visible.has(id) && !this.removing.has(id) : false } /** Startup: keep the holders a previous run left, without booting anything. */