fix(wsl-host): release a failed add's holder and cancel adds the experiment was turned off during (#29889)

A failed or deferred first add kept its detached distro holder with no record to find it; an add in flight when WSL hosts were turned off still finished as a ready host.
This commit is contained in:
Neil
2026-10-16 16:59:01 -07:00
committed by GitHub
parent 29765d4fd8
commit 93a5a1a20a
3 changed files with 113 additions and 16 deletions
+46
View File
@@ -0,0 +1,46 @@
/**
* The connect step of a first WSL host Add. Until the Add persists a record, nothing else (Remove,
* disabling the experiment, startup adoption) can find this distro's connection or detached
* holder, so every way the Add can end without a host must release them here.
*/
import { errorMessage } from '../../shared/error-message'
import type { WslHostAddResult } from '../../shared/wsl-host-types'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import type { WslHostConnector } from './wsl-host-service'
import { WslOrcadLaunchDeferredError, type WslOrcadConnection } from './wsl-orcad-connection'
type WslHostAddRefusal = Extract<WslHostAddResult, { ok: false }>
type AddConnector = Pick<WslHostConnector, 'connect' | 'disconnect' | 'releaseHolder'>
export async function abandonWslHostAdd(connector: AddConnector, distro: string): Promise<void> {
await connector.disconnect(distro).catch(() => {})
await connector.releaseHolder(distro).catch(() => {})
}
/** `lateRefusal` is re-read once the connect lands: the setting can change while it runs. */
export async function connectForWslHostAdd(
connector: AddConnector,
distro: string,
lateRefusal: () => WslHostAddRefusal | null
): Promise<{ connection: WslOrcadConnection } | WslHostAddRefusal> {
let connection: WslOrcadConnection
try {
connection = await connector.connect(distro)
} catch (error) {
await abandonWslHostAdd(connector, distro)
if (error instanceof WslOrcadLaunchDeferredError) {
return { ok: false, kind: 'deferred', message: error.launch.reason }
}
return {
ok: false,
kind: error instanceof OrcadHostUnsupportedError ? 'unsupported-wsl1' : 'failed',
message: errorMessage(error)
}
}
const refusal = lateRefusal()
if (refusal) {
await abandonWslHostAdd(connector, distro)
return refusal
}
return { connection }
}
+51
View File
@@ -424,6 +424,57 @@ describe('WslHostService', () => {
expect(connector.connect).not.toHaveBeenCalled()
})
it('releases the holder of a failed or deferred first add, which no record would ever find', async () => {
const connector = makeConnector([UBUNTU])
connector.connect.mockRejectedValueOnce(new Error('deploy failed'))
const service = makeService(connector)
await expect(service.add({ distro: 'Ubuntu' })).resolves.toMatchObject({ kind: 'failed' })
expect(connector.disconnect).toHaveBeenCalledWith('Ubuntu')
expect(connector.releaseHolder).toHaveBeenCalledWith('Ubuntu')
vi.clearAllMocks()
connector.connect.mockRejectedValueOnce(
new WslOrcadLaunchDeferredError({
outcome: 'deferred',
distro: 'Ubuntu',
candidateVersion: '2.0.0',
code: 'live-terminals',
reason: 'Terminals are still running.',
forceable: true
})
)
await expect(service.add({ distro: 'Ubuntu' })).resolves.toMatchObject({ kind: 'deferred' })
expect(connector.releaseHolder).toHaveBeenCalledWith('Ubuntu')
expect(listEnvironments(userDataPath)).toEqual([])
})
it('an add the experiment was turned off during ends without a host, connection or holder', async () => {
const connector = makeConnector([UBUNTU])
const service = makeService(connector)
let finishConnect: () => void = () => {}
connector.connect.mockImplementationOnce(async (distro: string) => {
await new Promise<void>((resolve) => {
finishConnect = resolve
})
return connection(distro)
})
const adding = service.add({ distro: 'Ubuntu' })
await vi.waitFor(() => expect(connector.connect).toHaveBeenCalled())
// Off and back on before the connect lands: the add started under the old setting.
enabled = false
await service.enabledChanged(false)
enabled = true
await service.enabledChanged(true)
finishConnect()
await expect(adding).resolves.toMatchObject({ ok: false, kind: 'disabled' })
expect(listEnvironments(userDataPath)).toEqual([])
expect(connector.disconnect).toHaveBeenCalledWith('Ubuntu')
expect(connector.releaseHolder).toHaveBeenCalledWith('Ubuntu')
expect(ready).not.toHaveBeenCalled()
})
it('treats a distro being added as known, so its first connect may spawn', async () => {
const connector = makeConnector([UBUNTU])
const service = makeService(connector)
+16 -16
View File
@@ -19,7 +19,6 @@ import {
type WslHostServerStop,
type WslHostStatus
} from '../../shared/wsl-host-types'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import type { WslHostDistroCandidate } from './wsl-host-distro-list'
import { wslHostSshManagedRefusal } from './wsl-host-ssh-orcad-conflict'
import { wslDistroHolderKey } from './wsl-distro-holder-record'
@@ -29,9 +28,11 @@ import {
sameWslDistro,
wslHostNotConnectedMessage,
WSL_HOST_REMOVING_MESSAGE,
WSL_HOSTS_DISABLED_MESSAGE,
wslHostRunRefusal
} from './wsl-host-add-admission'
import { WslOrcadLaunchDeferredError, type WslOrcadConnection } from './wsl-orcad-connection'
import type { WslOrcadConnection } from './wsl-orcad-connection'
import { abandonWslHostAdd, connectForWslHostAdd } from './wsl-host-first-connect'
import type { WslOrcadConnectionStatus } from './wsl-orcad-connection-status'
import { defaultWslOrcadEnvironmentId } from './wsl-orcad-deploy'
@@ -70,6 +71,8 @@ export class WslHostService {
private readonly adding = new Map<string, Promise<WslHostAddResult>>()
private visible = new Set<string>()
private readonly removing = new Set<string>()
// Bumped on disable so an Add that started before it cannot finish as a ready host.
private disableGeneration = 0
constructor(private readonly deps: WslHostServiceDependencies) {}
@@ -167,6 +170,7 @@ export class WslHostService {
this.setVisibleWork([...this.visible], new Set())
return
}
this.disableGeneration += 1
for (const record of this.records()) {
await connector.disconnect(record.distro)
await connector.releaseHolder(record.distro)
@@ -264,6 +268,7 @@ export class WslHostService {
if (refusal) {
return refusal
}
const generation = this.disableGeneration
const admission = admitWslHostDistro({
requested: args.distro,
candidates: await this.wsl.listDistros(),
@@ -282,19 +287,15 @@ export class WslHostService {
return sharing
}
const environmentId = defaultWslOrcadEnvironmentId(distro.name)
let connection: WslOrcadConnection
try {
connection = await this.wsl.connect(distro.name)
} catch (error) {
if (error instanceof WslOrcadLaunchDeferredError) {
return { ok: false, kind: 'deferred', message: error.launch.reason }
}
return {
ok: false,
kind: error instanceof OrcadHostUnsupportedError ? 'unsupported-wsl1' : 'failed',
message: errorMessage(error)
}
const connected = await connectForWslHostAdd(this.wsl, distro.name, () =>
generation === this.disableGeneration
? wslHostRunRefusal(this.deps)
: { ok: false, kind: 'disabled', message: WSL_HOSTS_DISABLED_MESSAGE }
)
if (!('connection' in connected)) {
return connected
}
const { connection } = connected
try {
addWslHostEnvironment(userDataPath, {
id: environmentId,
@@ -309,8 +310,7 @@ export class WslHostService {
}
})
} catch (error) {
await this.wsl.disconnect(distro.name).catch(() => {})
await this.wsl.releaseHolder(distro.name).catch(() => {})
await abandonWslHostAdd(this.wsl, distro.name)
return { ok: false, kind: 'failed', message: errorMessage(error) }
}
const record = this.recordForEnvironment(environmentId)