fix(wsl-host): uninstall and startup release WSL holders; a partial Remove keeps the host pending (#33843)

* fix(wsl-host): uninstall and startup release the WSL distro holders Orca left running

The detached wsl.exe holder outlives the app, so an uninstall left its distro,
orcad and shells running. The uninstaller now runs Orca with
--orca-uninstall-release-wsl-holders after killing the app and its daemon; it
ends only the holders this profile recorded, each verified by pid, start time
and command line, and keeps any it cannot verify.

At startup the WSL host service now releases holders whose distro is no longer
a host (a failed add or remove), and every holder while WSL hosts are off; a
closed gate releases them too.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(wsl-host): a Remove that can't prove the server stopped keeps the host, pending

A partial Remove dropped the host anyway, leaving a server whose live
terminals block the next add's activation. Remove now drops the host only on a
proven stop; otherwise it records a pending stop on the WSL record, keeps the
host listed and says why (live or unverifiable). Remove again retries without
ever booting a stopped distro (it says to start it); Connect or Add on the same
distro keeps the host; a distro that is no longer registered finishes. Nothing
reconnects a pending host on its own.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(wsl-host): E11 review — keep holders when records are unreadable; finish removals a stopped or renamed distro proves

- Startup keeps every holder when the WSL host records can't all be read
  (unverifiable), and no longer re-adopts a pending host's holder.
- Remove matches a renamed distro by GUID (isWslDistroRegistered), and a
  stopped distro finishes a pending Remove: the cold probe proves the stop.
- Only the user's Connect or Add withdraws a pending Remove; other connects
  are refused, and re-adding respects startIfStopped.
- Settings' Remove carries the kept result, so it warns instead of 'Removed'.
- A holder record without a start time is never killed on its command line
  alone, except the one this process just spawned.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(wsl-host): E11 review 2 — a closed gate keeps live hosts' holders; stop a renamed distro by its current name

- With the gate closed, a launch releases only orphan holders (no host record,
  or a pending Remove), under the same keep rule as startup; a user with no
  holder records costs one folder check, nothing else.
- The keep rule re-reads host records per holder and keeps an Add in flight.
- Remove resolves the distro's current name from its GUID and stops it there.
- The uninstall command arms its exit timer first and exits 1 if setup throws;
  NSIS launches it only when a userData holder folder exists.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(wsl-host): never boot a host whose Remove is pending at the next launch

Early wake built its host list from every record, so a host focused when its
Remove ended pending was booted at each launch, undoing the stop's proof. It
now skips pending records, and Remove forgets the host's early wake whatever
the outcome.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(wsl-host): no wake for a pending host; the e2e fake stops an unbridged server like the real one

- mayWake skips a host whose Remove is pending, so a due automation can't
  boot it (focus already couldn't).
- The e2e fake connector read a host with no bridge as stopped. Since E9 a
  re-enable connects nothing, so Remove after off/on left the host's shells
  running. Like the real connector, it now reconnects the always-running fake
  distro to stop its server.
- Status bookkeeping moves to WslHostStatusBoard, and the pending-host connect
  rule to admitConnectToPendingHost, to keep the service under max-lines.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-26 20:17:53 -07:00
committed by GitHub
co-authored by Claude
parent ea1cb2f18f
commit 284411d893
44 changed files with 1352 additions and 185 deletions
@@ -18,7 +18,8 @@ import { findCollidingEnvironmentName } from './runtime-environment-name-collisi
import {
readWslHostDeployments,
writeWslHostDeployment,
type WslHostDeploymentRecord
type WslHostDeploymentRecord,
type WslHostPendingStop
} from './wsl-host-deployment-store'
type WslHostPorts = Pick<WslHostDeploymentRecord, 'localPort' | 'remotePort'>
@@ -164,6 +165,20 @@ export function refreshWslHostPairing(
}
}
/** Marks (or with null, clears) a Remove that could not prove the host's server stopped. */
export function setWslHostPendingStop(
userDataPath: string,
environmentId: string,
pendingStop: WslHostPendingStop | null
): void {
const record = findWslHostDeployment(userDataPath, environmentId)
if (!record) {
throw new RuntimeEnvironmentStoreError('invalid_argument', 'This server is not a WSL host.')
}
const { pendingStop: _previous, ...rest } = record
writeWslHostDeployment(userDataPath, environmentId, pendingStop ? { ...rest, pendingStop } : rest)
}
/** Environment first: a crash between the writes leaves an ownerless record, never a bare server. */
export function removeWslHostEnvironment(userDataPath: string, environmentId: string): void {
const store = readPersistedEnvironmentStore(userDataPath)
+7
View File
@@ -10,6 +10,7 @@ import {
OrcadWslDeploymentLinkSchema,
type OrcadWslDeploymentLink
} from './wsl-host-deployment-link'
import type { WslHostServerStop } from './wsl-host-types'
export const RuntimeAccessEndpointSchema = z.object({
id: z.string().min(1),
@@ -162,6 +163,12 @@ export type PublicKnownRuntimeEnvironment = Omit<KnownRuntimeEnvironment, 'endpo
hostKeyFingerprint?: string
}
/** `serverStop` only for a host whose owning service stops a server (a WSL host, E11). */
export type RuntimeEnvironmentRemoval = {
removed: PublicKnownRuntimeEnvironment
serverStop?: WslHostServerStop
}
export function redactRuntimeEnvironment(
environment: KnownRuntimeEnvironment
): PublicKnownRuntimeEnvironment {
+12 -1
View File
@@ -18,6 +18,15 @@ const WSL_HOST_DEPLOYMENTS_FILE = 'orca-wsl-host-deployments.json'
const MAX_WSL_HOST_DEPLOYMENTS_FILE_BYTES = 256 * 1024
/** A managed orcad in a WSL distro. The environment's name is its label; runtime state lives elsewhere. */
/** A Remove that could not prove the host's server stopped (E11); the host stays until one does. */
export const WslHostPendingStopSchema = z.object({
since: z.number().finite(),
reason: z.string().max(4_096),
verdict: z.enum(['live', 'unverifiable'])
})
export type WslHostPendingStop = z.infer<typeof WslHostPendingStopSchema>
export const WslHostDeploymentRecordSchema = OrcadWslDeploymentLinkSchema.extend({
environmentId: z.string().min(1),
createdAt: z.number().finite(),
@@ -27,7 +36,8 @@ export const WslHostDeploymentRecordSchema = OrcadWslDeploymentLinkSchema.extend
*/
pairingPublicKeyB64: z.string().min(1).optional(),
/** The key a refresh replaced, accepted until the pairing write that follows it lands. */
previousPairingPublicKeyB64: z.string().min(1).optional()
previousPairingPublicKeyB64: z.string().min(1).optional(),
pendingStop: WslHostPendingStopSchema.optional()
})
export type WslHostDeploymentRecord = z.infer<typeof WslHostDeploymentRecordSchema>
@@ -194,6 +204,7 @@ export function overlayWslHostDeployments(
const {
environmentId: _environmentId,
createdAt: _createdAt,
pendingStop: _pendingStop,
pairingPublicKeyB64,
previousPairingPublicKeyB64,
...link
+10 -2
View File
@@ -28,6 +28,8 @@ export type WslHostStatus = {
detail: string | null
/** An app update the running server waits to take (e.g. live terminals); Update/Force retry it. */
deferredUpdate?: WslHostDeferredUpdate | null
/** A Remove that could not prove the server stopped; `detail` says why. Remove retries it. */
removalPending?: boolean
}
/** An explicit Update retries a skipped build behind the terminal census; Force also passes it. */
@@ -50,8 +52,14 @@ export type WslHostCapability = {
serverFiles: boolean
}
/** What removing a host left behind in its distro. */
export type WslHostServerStop = { stopped: true } | { stopped: false; reason: string }
/**
* What removing a host left behind in its distro, in the census vocabulary: `live` when something
* is proven still running, `unverifiable` when Orca could not tell. `kept` (E11): the host stays
* listed with its stop pending, so Remove can retry; older mains removed it anyway.
*/
export type WslHostServerStop =
| { stopped: true }
| { stopped: false; reason: string; verdict?: 'live' | 'unverifiable'; kept?: boolean }
export type WslHostAddFailureKind =
| 'unsupported-wsl1'