fix(wsl-host): tell users how to fix a distro Orca can't host (D1c-1) (#33085)

* fix(wsl-host): tell users how to fix a distro Orca can't host

With the WSL bridge going away there is no fallback for a distro Orca's server can't
run in, so each refusal now names the step that fixes it:
- WSL 1: the wsl --set-version command, also shown in the Add host form, where the
  disabled option could never show it.
- An unregistered distro, at Add and at connect: how to list installed distros.
- NixOS's stub loader: turn on nix-ld.
- glibc below every runtime's floor: the minimum release to use.
- A WSL distro with an unknown platform no longer reads as "this SSH host".
Connect refusals other than WSL 1 report as 'unsupported-distro' instead of reusing
'unsupported-wsl1'.

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

* fix(wsl-host): show only the plain message when a WSL host can't connect or be added

* test(wsl-host): fold the plain-message case into the existing readIpcErrorDetail tests

* fix(wsl-host): keep SSH's old-glibc and NixOS refusals as they were; add the fix only for a WSL distro

The glibc and nix-ld hints were appended in code SSH hosts share, so SSH users
saw new wording with the WSL hosts gate closed. The shared messages are back to
main's text; the WSL add flow adds the fix to its own refusal.

(cherry picked from commit c7a07b61c440e87652ca956570b8b91492c2a375)

* refactor(wsl): map a missing distro in the unsupported-distro module, keeping orcad deploy under its line cap

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-28 04:17:40 -07:00
committed by GitHub
co-authored by Claude OrcaWin
parent 2459222db1
commit 0b6071aec4
16 changed files with 237 additions and 21 deletions
@@ -56,7 +56,11 @@ export async function resolveOrcadRemoteContext<Target extends OrcadHostTarget =
): Promise<OrcadRemoteContext<Target>> {
const host = detectedHost ?? (await detectRemoteHostPlatform(connection, { signal }))
if (!host) {
throw new OrcadHostUnsupportedError('Orca’s server doesn’t support this SSH host’s platform.')
throw new OrcadHostUnsupportedError(
isWslOrcadHostTarget(target)
? 'Orca’s server doesn’t support this WSL distro’s platform. Use an x64 or arm64 Linux distro.'
: 'Orca’s server doesn’t support this SSH host’s platform.'
)
}
const remote = { conn: connection, host, signal }
const { orcadHome: remoteHome, userDataDir } = await resolveOrcadMachineHome(
@@ -5,6 +5,13 @@ import type { OrcadHostTransport } from '../../ssh/orcad-host-transport'
import { pinnedRuntimeTargetForHost } from '../../ssh/pinned-runtime-host-target'
import type { RemoteHostPlatform } from '../../ssh/ssh-remote-platform'
/** Lets a caller that knows the host kind add its own fix; the message is the one SSH shows. */
export class OrcadHostGlibcUnsupportedError extends OrcadHostUnsupportedError {
constructor(target: string, glibc: string) {
super(`No Orca runtime supports ${target} with glibc ${glibc}.`)
}
}
export async function resolveOrcadRuntimeTarget(options: {
conn: OrcadHostTransport
host: RemoteHostPlatform
@@ -15,9 +22,7 @@ export async function resolveOrcadRuntimeTarget(options: {
const target = pinnedRuntimeTargetForHost(facts)
if (!target) {
const glibc = facts.glibc ? `${facts.glibc.major}.${facts.glibc.minor}` : 'unknown'
throw new OrcadHostUnsupportedError(
`No Orca runtime supports ${facts.target} with glibc ${glibc}.`
)
throw new OrcadHostGlibcUnsupportedError(facts.target, glibc)
}
return target
}
@@ -0,0 +1,68 @@
import { describe, expect, it } from 'vitest'
import { classifyOrcadHostUnavailable } from '../orcad/orcad-host-unavailable'
import { OrcadHostGlibcUnsupportedError } from '../managed-server/lifecycle/orcad-runtime-target'
import { RemoteNodeRuntimeSelfTestError } from '../ssh/orcad-remote-node-runtime-report'
import {
isWslDistroNotFoundFailure,
WslDistroNotRegisteredError,
WslDistroVersion1Error,
wslUnsupportedDistroFixMessage
} from './wsl-distro-unsupported'
const NIXOS_STUB_OUTPUT =
'Could not start dynamically linked executable: node\n' +
'NixOS cannot run dynamically linked executables intended for generic\n' +
'linux environments out of the box.'
describe('distros that cannot host Orca', () => {
it('says how to convert a WSL 1 distro', () => {
expect(new WslDistroVersion1Error('Legacy').message).toContain('wsl --set-version Legacy 2')
})
it('classifies both refusals as an unsupported host, as the SSH path records them', () => {
expect(classifyOrcadHostUnavailable(new WslDistroVersion1Error('Legacy'))).toBe(
'unsupported_host'
)
expect(classifyOrcadHostUnavailable(new WslDistroNotRegisteredError('Gone'))).toBe(
'unsupported_host'
)
})
it.each([
['the English sentence', 'There is no distribution with the supplied name.', true],
[
'the error code under a localized sentence',
'Fehler: Wsl/Service/WSL_E_DISTRO_NOT_FOUND',
true
],
['an unrelated failure', 'Command "uname -r" failed (exit 1): timeout', false]
])('recognizes a missing distro from %s', (_case, message, expected) => {
expect(isWslDistroNotFoundFailure(new Error(message))).toBe(expected)
})
it('adds the glibc fix for a distro, leaving the shared refusal SSH shows as it was', () => {
const error = new OrcadHostGlibcUnsupportedError('linux-arm64-glibc', '2.27')
expect(error.name).toBe('OrcadHostUnsupportedError')
expect(error.message).toBe('No Orca runtime supports linux-arm64-glibc with glibc 2.27.')
expect(wslUnsupportedDistroFixMessage(error)).toBe(
'No Orca runtime supports linux-arm64-glibc with glibc 2.27. Use a Linux release with ' +
'glibc 2.28 or newer (for example Ubuntu 20.04 or Debian 10 and later).'
)
})
it('tells a NixOS distro to turn on nix-ld, leaving the shared self-test wording as it was', () => {
const error = new RemoteNodeRuntimeSelfTestError(127, NIXOS_STUB_OUTPUT)
expect(error.message).toMatch(/^The pinned Node runtime did not run on the host \(exit 127\)/)
const fix = wslUnsupportedDistroFixMessage(error)
expect(fix).toMatch(/^NixOS can’t run Orca’s Node runtime/)
expect(fix).toContain('programs.nix-ld.enable = true;')
expect(fix).toContain(`(exit 127: ${NIXOS_STUB_OUTPUT})`)
})
it('has no fix for other failures', () => {
expect(
wslUnsupportedDistroFixMessage(new RemoteNodeRuntimeSelfTestError(139, 'Segmentation fault'))
).toBeNull()
expect(wslUnsupportedDistroFixMessage(new Error('bridge refused'))).toBeNull()
})
})
+70
View File
@@ -0,0 +1,70 @@
/**
* Distros that can't host Orca's server, each refused with the step that fixes it. There is no
* fallback once the WSL bridge is gone, so a refusal must say what to run, not only what failed.
*/
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import { OrcadHostGlibcUnsupportedError } from '../managed-server/lifecycle/orcad-runtime-target'
import { RemoteNodeRuntimeSelfTestError } from '../ssh/orcad-remote-node-runtime-report'
export function wsl1DistroMessage(distro: string): string {
return (
`${distro} runs WSL 1, which Orca no longer supports. ` +
`Convert it to WSL 2 in a Windows terminal with: wsl --set-version ${distro} 2`
)
}
export function wslDistroNotRegisteredMessage(distro: string): string {
return (
`${distro} is not an installed WSL distro. ` +
'Run wsl --list --verbose in a Windows terminal to see the installed ones, ' +
'or reinstall it and try again.'
)
}
/** Keeps OrcadHostUnsupportedError's name, so callers that classify by name still see it. */
export class WslDistroVersion1Error extends OrcadHostUnsupportedError {
constructor(distro: string) {
super(wsl1DistroMessage(distro))
}
}
export class WslDistroNotRegisteredError extends OrcadHostUnsupportedError {
constructor(distro: string) {
super(wslDistroNotRegisteredMessage(distro))
}
}
// wsl.exe localizes its sentence but not the error code that newer builds print after it.
const DISTRO_NOT_FOUND = /WSL_E_DISTRO_NOT_FOUND|There is no distribution with the supplied name/i
export function isWslDistroNotFoundFailure(error: unknown): boolean {
return error instanceof Error && DISTRO_NOT_FOUND.test(error.message)
}
/** Rethrows wsl.exe's missing-distro failure as the refusal that says how to fix it. */
export function refuseMissingWslDistro(distro: string): (error: unknown) => never {
return (error) => {
throw isWslDistroNotFoundFailure(error) ? new WslDistroNotRegisteredError(distro) : error
}
}
// NixOS's stub loader refuses generic Linux binaries with this sentence.
const NIXOS_STUB_LOADER = /NixOS cannot run dynamically linked executables/
/** The shared server refusal plus the step that fixes it in a distro; null when there is none. */
export function wslUnsupportedDistroFixMessage(error: unknown): string | null {
if (error instanceof OrcadHostGlibcUnsupportedError) {
return (
`${error.message} Use a Linux release with glibc 2.28 or newer ` +
'(for example Ubuntu 20.04 or Debian 10 and later).'
)
}
if (error instanceof RemoteNodeRuntimeSelfTestError && NIXOS_STUB_LOADER.test(error.output)) {
return (
'NixOS can’t run Orca’s Node runtime as it is. Turn on nix-ld ' +
'(programs.nix-ld.enable = true; in configuration.nix), run nixos-rebuild switch, ' +
`then reconnect. (exit ${error.exitStatus ?? 'unknown'}: ${error.output})`
)
}
return null
}
+3 -2
View File
@@ -2,6 +2,7 @@
import type { WslHostAddResult } from '../../shared/wsl-host-types'
import type { WslHostDistroCandidate } from './wsl-host-distro-list'
import { wslDistroHolderKey } from './wsl-distro-holder-record'
import { wsl1DistroMessage, wslDistroNotRegisteredMessage } from './wsl-distro-unsupported'
export const WSL_HOSTS_DISABLED_MESSAGE = 'WSL hosts are turned off in Settings → Experimental.'
export const WSL_HOST_SERVER_FILES_MISSING_MESSAGE =
@@ -53,14 +54,14 @@ export function admitWslHostDistro(args: {
return {
ok: false,
kind: 'failed',
message: `${args.requested} is not a registered WSL distro.`
message: wslDistroNotRegisteredMessage(args.requested)
}
}
if (distro.version === 1) {
return {
ok: false,
kind: 'unsupported-wsl1',
message: `${distro.name} runs WSL 1, which isn’t supported as a host. Convert it with: wsl --set-version ${distro.name} 2`
message: wsl1DistroMessage(distro.name)
}
}
if (args.alreadyAdded(distro.name)) {
+15 -2
View File
@@ -4,6 +4,8 @@ import type { WslHostAddResult } from '../../shared/wsl-host-types'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import type { WslHostConnector } from './wsl-host-service'
import { WslOrcadLaunchDeferredError } from './wsl-orcad-connection-errors'
import { WslDistroVersion1Error, wslUnsupportedDistroFixMessage } from './wsl-distro-unsupported'
import { RemoteNodeRuntimeSelfTestError } from '../ssh/orcad-remote-node-runtime-report'
import type { WslOrcadConnection } from './wsl-orcad-connection'
type WslHostAddRefusal = Extract<WslHostAddResult, { ok: false }>
@@ -30,8 +32,8 @@ export async function connectForWslHostAdd(
}
return {
ok: false,
kind: error instanceof OrcadHostUnsupportedError ? 'unsupported-wsl1' : 'failed',
message: errorMessage(error)
kind: connectFailureKind(error),
message: wslUnsupportedDistroFixMessage(error) ?? errorMessage(error)
}
}
const refusal = lateRefusal()
@@ -41,3 +43,14 @@ export async function connectForWslHostAdd(
}
return { connection }
}
/** A distro Orca can't run in reads apart from a connect that may work on retry. */
function connectFailureKind(error: unknown): WslHostAddRefusal['kind'] {
if (error instanceof WslDistroVersion1Error) {
return 'unsupported-wsl1'
}
return error instanceof OrcadHostUnsupportedError ||
error instanceof RemoteNodeRuntimeSelfTestError
? 'unsupported-distro'
: 'failed'
}
+12 -3
View File
@@ -13,6 +13,8 @@ import { getWslHostDeploymentStorePath } from '../../shared/wsl-host-deployment-
import { getPreferredPairingOffer } from '../../shared/runtime-environments'
import type { WslHostStatus } from '../../shared/wsl-host-types'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import { RemoteNodeRuntimeSelfTestError } from '../ssh/orcad-remote-node-runtime-report'
import { wsl1DistroMessage, WslDistroVersion1Error } from './wsl-distro-unsupported'
import { WslHostService, type WslHostConnector } from './wsl-host-service'
import { wakeRememberedWslHosts } from './wsl-host-early-wake'
import { WslOrcadLaunchDeferredError } from './wsl-orcad-connection-errors'
@@ -773,10 +775,17 @@ describe('WslHostService', () => {
})
it.each([
['unsupported-wsl1', () => new WslDistroVersion1Error('Ubuntu'), wsl1DistroMessage('Ubuntu')],
[
'unsupported-wsl1',
() => new OrcadHostUnsupportedError('WSL 1 cannot run the Orca server.'),
'WSL 1 cannot run the Orca server.'
'unsupported-distro',
() => new OrcadHostUnsupportedError('No Orca runtime supports linux-arm64-glibc.'),
'No Orca runtime supports linux-arm64-glibc.'
],
[
'unsupported-distro',
() =>
new RemoteNodeRuntimeSelfTestError(127, 'libstdc++.so.6: cannot open shared object file'),
expect.stringContaining('apk add libstdc++')
],
['failed', () => new Error('bridge refused'), 'bridge refused']
])(
+19 -1
View File
@@ -209,7 +209,25 @@ describe('deployAndLaunchWslOrcad', () => {
it('refuses a WSL 1 distro before holding it or touching its files', async () => {
await expect(launch('4.4.0-19041-Microsoft')).rejects.toMatchObject({
name: 'OrcadHostUnsupportedError'
name: 'OrcadHostUnsupportedError',
message: expect.stringContaining('wsl --set-version Ubuntu-24.04 2')
})
expect(mocks.steps).toEqual(['uname'])
})
it('tells an unregistered distro how to find the installed ones, before holding anything', async () => {
mocks.exec.mockImplementation(async () => {
mocks.steps.push('uname')
throw new Error(
'Command "uname -r" failed (exit 1): There is no distribution with the supplied name.\n' +
'Error code: Wsl/Service/WSL_E_DISTRO_NOT_FOUND'
)
})
await expect(
deployAndLaunchWslOrcad('Gone', { holder: holder(), createTransport: transport })
).rejects.toMatchObject({
name: 'OrcadHostUnsupportedError',
message: expect.stringContaining('wsl --list --verbose')
})
expect(mocks.steps).toEqual(['uname'])
})
+3 -6
View File
@@ -8,7 +8,6 @@ import { recoverInterruptedOrcadActivation } from '../managed-server/activation/
import { readOrcadActivationRecord } from '../managed-server/activation/orcad-activation-record-store'
import { readOrcadActivationTransaction } from '../managed-server/activation/orcad-activation-transaction-store'
import { materializeOrcadArtifact } from '../orcad/orcad-artifact-materializer'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import type { OrcadHostTransport } from '../ssh/orcad-host-transport'
import { ORCAD_ACTIVATION_POLICY_REFUSED_CODE } from '../ssh/orcad-installed-activation'
import { orcadBoundPort } from '../managed-server/lifecycle/orcad-managed-bound-port'
@@ -38,6 +37,7 @@ import { joinRemotePath } from '../ssh/ssh-remote-platform'
import { readLocalFullVersion } from '../managed-server/install/remote-versioned-install'
import { createWslDistroHolder, type WslDistroHolder } from './wsl-distro-holder'
import { wslDistroHolderKey } from './wsl-distro-holder-record'
import { refuseMissingWslDistro, WslDistroVersion1Error } from './wsl-distro-unsupported'
import { readWslDistroRegistryIdShared } from './wsl-distro-registry-id'
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
import { createWslOrcadHostTransport, type WslOrcadHostTransport } from './wsl-orcad-host-transport'
@@ -134,12 +134,9 @@ async function launchOverTransport(
const { signal } = options
let ownBuildVersion: string | null = null
const conn = withOwnBuildLaunchEnv(transport, () => ownBuildVersion)
const kernel = await execCommand(conn, 'uname -r', { signal })
const kernel = await execCommand(conn, 'uname -r', { signal }).catch(refuseMissingWslDistro(name))
if (isWsl1KernelRelease(kernel)) {
throw new OrcadHostUnsupportedError(
`The WSL 1 distro ${name} isn’t supported by Orca’s server. ` +
`Convert it with: wsl --set-version ${name} 2`
)
throw new WslDistroVersion1Error(name)
}
// Acquire a holder before launch or WSL can idle-stop orcad's distro.
const holder = options.holder ?? createWslDistroHolder({ userDataPath: userDataPath() })
@@ -44,6 +44,7 @@ import {
} from '../wsl-move/wsl-move-orchestrator-status'
import { openWslMoveDialogFor } from '../wsl-move/wsl-move-entry'
import { WslMoveUnfinishedNotice } from '../wsl-move/WslMoveUnfinishedNotice'
import { readIpcErrorDetail } from '@/lib/ipc-error'
function getWslHostRowStatus(
status: WslHostStatus | undefined,
@@ -118,7 +119,8 @@ export function WslHostsSection({
await api.connect({ environmentId, ...(intent ? { intent } : {}) })
onChanged()
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error))
// The main process's message is for the user; Electron's wrapper and the class name aren't.
toast.error(readIpcErrorDetail(error) ?? String(error))
} finally {
setConnectingId(null)
}
@@ -5,6 +5,7 @@ import { useAppStore } from '@/store'
import { AddWslHostFormPanel, canAddWslDistro } from './AddWslHostFormPanel'
import { useWslDistroList } from './use-wsl-distro-list'
import { useWslHostServerFilesMissing } from './use-wsl-hosts-enabled'
import { readIpcErrorDetail } from '@/lib/ipc-error'
export function AddWslHostDialogBody({
adding,
@@ -64,7 +65,7 @@ export function AddWslHostDialogBody({
)
onAdded()
} catch (cause) {
setError(cause instanceof Error ? cause.message : String(cause))
setError(readIpcErrorDetail(cause) ?? String(cause))
} finally {
onAddingChange(false)
}
@@ -50,6 +50,11 @@ describe('AddWslHostFormPanel', () => {
expect(html).toContain('until you move them to this host')
})
it('spells out the WSL 2 conversion for a WSL 1 distro the list cannot offer', () => {
expect(render()).toContain('wsl --set-version Legacy 2')
expect(render({ distros: [ubuntu] })).not.toContain('wsl --set-version')
})
it('says this build cannot run WSL hosts and offers nothing to add', () => {
const html = render({ selectedDistro: 'Ubuntu', serverFilesMissing: true })
expect(html).toContain('This build of Orca can’t run WSL hosts (server files missing).')
@@ -58,6 +58,8 @@ export function AddWslHostFormPanel({
onCancel: () => void
}): React.JSX.Element {
const selected = distros.find((distro) => distro.name === selectedDistro)
// A disabled option can't show its refusal, so the conversion command is spelled out here.
const wsl1Distro = distros.find((distro) => distro.version === 1)
const stopped = selected?.state === 'stopped' && canAddWslDistro(selected)
return (
<>
@@ -134,6 +136,15 @@ export function AddWslHostFormPanel({
)}
</p>
) : null}
{wsl1Distro ? (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.sidebar.AddWslHostFormPanel.wsl1Hint',
'WSL 1 distros can’t be hosts. Convert one in a Windows terminal with: wsl --set-version {{value0}} 2',
{ value0: wsl1Distro.name }
)}
</p>
) : null}
{stopped ? (
<p className="text-xs text-muted-foreground">
{translate(
+2 -1
View File
@@ -6573,7 +6573,8 @@
"adding": "Setting up…",
"startAndAdd": "Start and add",
"add": "Add host",
"refresh": "Refresh"
"refresh": "Refresh",
"wsl1Hint": "WSL 1 distros can’t be hosts. Convert one in a Windows terminal with: wsl --set-version {{value0}} 2"
},
"wslHostRowStatus": {
"connecting": "Starting",
+9
View File
@@ -99,6 +99,15 @@ describe('readIpcErrorDetail', () => {
)
).toBe('SSH connection failed: Relay package not found.')
})
it("leaves only an unsupported distro's fix for the user", () => {
const error = new Error(
"Error invoking remote method 'wslHosts:connect': OrcadHostUnsupportedError: Ubuntu-18.04 has glibc 2.27; Orca's WSL host needs 2.28 or newer. Upgrade the distro, or keep this project on Windows."
)
expect(readIpcErrorDetail(error)).toBe(
"Ubuntu-18.04 has glibc 2.27; Orca's WSL host needs 2.28 or newer. Upgrade the distro, or keep this project on Windows."
)
})
})
// Typed main-process class names must not obscure actionable worktree-removal refusal copy (#19334).
+2
View File
@@ -68,6 +68,8 @@ export type WslHostServerStop =
export type WslHostAddFailureKind =
| 'unsupported-wsl1'
/** Its platform, libc or loader can't run Orca's server (e.g. NixOS, musl without libstdc++). */
| 'unsupported-distro'
| 'distro-stopped'
| 'already-added'
| 'ssh-managed'