Files
orca/src/preload/api/runtime-api.ts
T
NeilandClaude 284411d893 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>
2026-10-26 20:17:53 -07:00

180 lines
7.0 KiB
TypeScript

import type { ManagedOrcadPreloadApi } from './managed-orcad-api'
import type { WslHostsPreloadApi } from './wsl-hosts-api'
import type { RuntimeHostStatusSnapshot } from '../../shared/runtime-host-status'
import type {
RuntimeBrowserDriverState,
RuntimeRendererSyncWindowGraph,
RuntimeStatus,
RuntimeSyncWindowGraphResult,
RuntimeTerminalDriverState
} from '../../shared/runtime-types'
import type { RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope'
import type { ClientHostedBrowserRowsEvent } from '../../shared/client-hosted-browser-rows'
import type { SidebarInputsChangedEvent } from '../../shared/sidebar-list/sidebar-inputs-feed-contract'
import type {
PublicKnownRuntimeEnvironment,
RuntimeEnvironmentRemoval
} from '../../shared/runtime-environments'
import type { VerifyAndAddRuntimeEnvironmentResult } from '../../shared/remote-pairing-verification'
import type {
BrowserClientHostPlacementPreparationRequest,
BrowserPageCreationPlacement
} from '../../shared/browser-client-host-placement'
import type { RemoteRuntimeSharedConnectionDiagnostics } from '../../shared/remote-runtime/control/remote-runtime-shared-control-types'
import type { WslOrcadAccountNotAppliedEvent } from '../../shared/wsl-orcad-account-notice'
export type RuntimeEnvironmentSubscriptionHandle = {
unsubscribe: () => void
sendBinary: (bytes: Uint8Array<ArrayBufferLike>) => void
}
export type RuntimeApi = {
runtime: {
syncWindowGraph: (
graph: RuntimeRendererSyncWindowGraph
) => Promise<RuntimeSyncWindowGraphResult>
getStatus: () => Promise<RuntimeStatus>
call: (args: { method: string; params?: unknown }) => Promise<RuntimeRpcResponse<unknown>>
subscribe: (
args: { method: string; params?: unknown },
callback: (response: RuntimeRpcResponse<unknown>) => void
) => Promise<RuntimeEnvironmentSubscriptionHandle>
getTerminalFitOverrides: () => Promise<
{ ptyId: string; mode: 'mobile-fit' | 'remote-desktop-fit'; cols: number; rows: number }[]
>
getTerminalDrivers: () => Promise<
{
ptyId: string
driver: RuntimeTerminalDriverState
}[]
>
getBrowserDrivers: () => Promise<
{
browserPageId: string
driver: RuntimeBrowserDriverState
}[]
>
getBrowserRemoteViewerPages?: () => Promise<string[]>
getClientHostedBrowserRows: () => Promise<ClientHostedBrowserRowsEvent[]>
restoreTerminalFit: (ptyId: string) => Promise<{ restored: boolean }>
reclaimBrowserForDesktop: (browserPageId: string) => Promise<{ reclaimed: boolean }>
onTerminalFitOverrideChanged: (
callback: (event: {
ptyId: string
mode: 'mobile-fit' | 'remote-desktop-fit' | 'desktop-fit'
cols: number
rows: number
}) => void
) => () => void
onTerminalDriverChanged: (
callback: (event: { ptyId: string; driver: RuntimeTerminalDriverState }) => void
) => () => void
onNativeChatLaunchDraftResolved?: (
callback: (event: { tabId: string; text: string; createdAt: number }) => void
) => () => void
onBrowserDriverChanged: (
callback: (event: { browserPageId: string; driver: RuntimeBrowserDriverState }) => void
) => () => void
// Older preloads lack this signal.
onBrowserRemoteViewersChanged?: (
callback: (event: { browserPageId: string; hasRemoteViewers: boolean }) => void
) => () => void
onClientHostedBrowserRowsChanged: (
callback: (event: ClientHostedBrowserRowsEvent) => void
) => () => void
onSidebarInputsChanged: (callback: (event: SidebarInputsChangedEvent) => void) => () => void
}
runtimeEnvironments: {
desktopCreatedChatIds?: () => Promise<string[]>
getStatusSnapshots: () => Promise<RuntimeHostStatusSnapshot[]>
onStatusChanged: (callback: (snapshot: RuntimeHostStatusSnapshot) => void) => () => void
// Absent in the web client, whose catalog lives in the browser rather than a CLI-editable file.
onCatalogChanged?: (callback: () => void) => () => void
list: () => Promise<PublicKnownRuntimeEnvironment[]>
addFromPairingCode: (args: {
name: string
pairingCode: string
}) => Promise<{ environment: PublicKnownRuntimeEnvironment }>
verifyAndAddFromPairingCode: (args: {
name: string
pairingCode: string
allowLoopback?: boolean
}) => Promise<VerifyAndAddRuntimeEnvironmentResult>
/** Replaces a revoked pairing in place; refuses a link for a different server. */
repairFromPairingCode: (args: {
selector: string
pairingCode: string
}) => Promise<VerifyAndAddRuntimeEnvironmentResult>
resolve: (args: { selector: string }) => Promise<PublicKnownRuntimeEnvironment>
remove: (args: { selector: string }) => Promise<RuntimeEnvironmentRemoval>
disconnect: (args: {
selector: string
}) => Promise<{ disconnected: PublicKnownRuntimeEnvironment }>
connect: (args: {
selector: string
timeoutMs?: number
}) => Promise<RuntimeRpcResponse<RuntimeStatus>>
getStatus: (args: {
selector: string
timeoutMs?: number
observeOnly?: true
}) => Promise<RuntimeRpcResponse<RuntimeStatus>>
onSharedControlDiagnostics?: (
callback: (event: {
environmentId: string
transportGeneration: number
diagnostics: RemoteRuntimeSharedConnectionDiagnostics
}) => void
) => () => void
// Absent in the web client, which has no desktop account shim.
onWslAccountNotApplied?: (
callback: (event: WslOrcadAccountNotAppliedEvent) => void
) => () => void
prepareBrowserClientHostPlacement: (
args: BrowserClientHostPlacementPreparationRequest
) => Promise<BrowserPageCreationPlacement>
// Resume/online events advance only pending shared-control reconnect timers.
retryConnectionsNow?: () => Promise<void>
call: (args: {
selector: string
method: string
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedEnvironmentRuntimeId?: string
}) => Promise<RuntimeRpcResponse<unknown>>
cancelSubscription: (args: { subscriptionId: string }) => Promise<void>
subscribe: (
args: {
subscriptionId?: string
selector: string
method: string
params?: unknown
timeoutMs?: number
expectedEnvironmentPairingRevision?: number
expectedEnvironmentRuntimeId?: string
},
callbacks: {
onResponse: (response: RuntimeRpcResponse<unknown>) => void
onBinary?: (bytes: Uint8Array<ArrayBufferLike>) => void
onError?: (error: { code: string; message: string }) => void
onClose?: () => void
}
) => Promise<RuntimeEnvironmentSubscriptionHandle>
// Only desktop clients deploy servers over SSH.
managedOrcad?: ManagedOrcadPreloadApi
// Only Windows desktops (or e2e fakes) host WSL distros.
wslHosts?: WslHostsPreloadApi
}
wsl: {
isAvailable: () => Promise<boolean>
listDistros: () => Promise<string[]>
}
pwsh: {
isAvailable: () => Promise<boolean>
}
gitBash: {
isAvailable: () => Promise<boolean>
}
}