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.
This commit is contained in:
Neil
2026-10-16 19:35:11 -07:00
committed by GitHub
parent 49450b7144
commit aa05edb8d6
2 changed files with 36 additions and 11 deletions
+20
View File
@@ -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)
+16 -11
View File
@@ -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. */