fix(wsl): a held pane whose distro could not be checked says so, not that it stopped (#35860)

This commit is contained in:
Neil
2026-10-29 02:04:09 -07:00
committed by GitHub
parent f8cf31797e
commit cfdd7d6720
21 changed files with 170 additions and 49 deletions
@@ -5,7 +5,7 @@ import type * as DistroGate from '../wsl-background-distro-gate'
import type { DaemonServer } from './daemon-server'
import type { DaemonPtyAdapter } from './daemon-pty-adapter'
import { createMockSubprocess, startDaemonAdapterHarness } from './daemon-pty-adapter-test-harness'
import { parseWslDistroStoppedHold } from '../../shared/wsl-distro-stopped-hold'
import { parseWslDistroHold } from '../../shared/wsl-distro-stopped-hold'
const readState = vi.hoisted(() =>
vi.fn(
@@ -54,7 +54,10 @@ describe('a passive restore of a WSL bridge pane (E9)', () => {
it('is held, starting nothing, when a settled read finds the distro stopped', async () => {
const error = await restore({ wslPassiveRestore: true }).catch((caught: unknown) => caught)
expect(error instanceof Error ? parseWslDistroStoppedHold(error.message) : null).toBe('Ubuntu')
expect(error instanceof Error ? parseWslDistroHold(error.message) : null).toEqual({
distro: 'Ubuntu',
state: 'stopped'
})
expect(spawns).toBe(0)
expect(readState).toHaveBeenCalledWith('Ubuntu', { retryUnverifiable: true })
})
@@ -69,7 +72,11 @@ describe('a passive restore of a WSL bridge pane (E9)', () => {
it('holds when the read cannot tell, too', async () => {
readState.mockResolvedValue('unverifiable')
const error = await restore({ wslPassiveRestore: true }).catch((caught: unknown) => caught)
expect(error instanceof Error ? parseWslDistroStoppedHold(error.message) : null).toBe('Ubuntu')
// Named as such, so the card says Orca couldn't check rather than that the distro stopped.
expect(error instanceof Error ? parseWslDistroHold(error.message) : null).toEqual({
distro: 'Ubuntu',
state: 'unverifiable'
})
expect(spawns).toBe(0)
})
@@ -1,5 +1,5 @@
import { readWslDistroRunningState } from '../wsl-background-distro-gate'
import { wslDistroStoppedHoldMessage } from '../../shared/wsl-distro-stopped-hold'
import { wslDistroHoldMessage } from '../../shared/wsl-distro-stopped-hold'
import type { PtySpawnOptions } from '../providers/pty-provider-contract'
/**
@@ -20,7 +20,8 @@ export async function holdPassiveRestoreOfStoppedWslDistro(
) {
return
}
if ((await readWslDistroRunningState(wslDistro, { retryUnverifiable: true })) !== 'running') {
throw new Error(wslDistroStoppedHoldMessage(wslDistro))
const state = await readWslDistroRunningState(wslDistro, { retryUnverifiable: true })
if (state !== 'running') {
throw new Error(wslDistroHoldMessage(wslDistro, state))
}
}
@@ -4,10 +4,13 @@ import { RemoteRuntimeRecoveryCard } from './RemoteRuntimeRecoveryCard'
/** A pane whose WSL distro is stopped: nothing restarts it on its own (E9); Start is the user's act. */
export function WslDistroStoppedCard({
distro,
unverifiable = false,
onStart,
data
}: {
distro: string
/** The running read gave no answer: say so rather than claim the distro stopped. */
unverifiable?: boolean
onStart: () => void
data: Record<`data-${string}`, string>
}): React.JSX.Element {
@@ -15,9 +18,17 @@ export function WslDistroStoppedCard({
<RemoteRuntimeRecoveryCard
icon="stopped"
data={data}
title={translate('components.native-chat.hostOutage.stopped', '{{distro}} is stopped', {
distro
})}
title={
unverifiable
? translate(
'components.native-chat.hostOutage.unverifiable',
"Orca couldn't check whether {{distro}} is running",
{ distro }
)
: translate('components.native-chat.hostOutage.stopped', '{{distro}} is stopped', {
distro
})
}
action={{
label: translate('components.native-chat.hostOutage.start', 'Start'),
onClick: onStart
@@ -0,0 +1,50 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { makePaneKey } from '../../../../shared/stable-pane-id'
import { WslDistroStoppedHoldCard } from './WslDistroStoppedHoldCard'
import {
holdPaneForStoppedWslDistro,
wslDistroStoppedHoldStore
} from './wsl-distro-stopped-hold-state'
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string, options?: Record<string, string>) =>
fallback.replace(/\{\{(\w+)\}\}/g, (_match, name: string) => options?.[name] ?? ''),
getIntlLocale: () => 'en-US'
}))
const TAB = '11111111-1111-4111-8111-111111111111'
const LEAF = '22222222-2222-4222-8222-222222222222'
afterEach(() => {
cleanup()
wslDistroStoppedHoldStore.setState({ panes: {} })
})
function holdPane(state: 'stopped' | 'unverifiable') {
const release = vi.fn(async () => {})
holdPaneForStoppedWslDistro(makePaneKey(TAB, LEAF), { distro: 'Ubuntu', state, release })
render(<WslDistroStoppedHoldCard tabId={TAB} leafId={LEAF} />)
return release
}
describe('a restored WSL bridge pane held for its distro (E9)', () => {
it('says the distro is stopped when the read found it stopped', () => {
holdPane('stopped')
expect(screen.getByText('Ubuntu is stopped')).toBeInTheDocument()
})
// The read gave no answer, so the card must not claim the distro stopped.
it("says Orca couldn't check when the read could not tell, and still offers Start", async () => {
const release = holdPane('unverifiable')
expect(screen.getByText("Orca couldn't check whether Ubuntu is running")).toBeInTheDocument()
expect(screen.queryByText('Ubuntu is stopped')).not.toBeInTheDocument()
await userEvent.setup().click(screen.getByRole('button', { name: 'Start' }))
expect(release).toHaveBeenCalledOnce()
})
})
@@ -13,15 +13,19 @@ export function WslDistroStoppedHoldCard({
tabId: string
leafId: string
}): React.JSX.Element | null {
const distro = useWslDistroStoppedHold(makePaneKey(tabId, leafId))
if (!distro) {
const hold = useWslDistroStoppedHold(makePaneKey(tabId, leafId))
if (!hold) {
return null
}
return (
<WslDistroStoppedCard
distro={distro}
data={{ 'data-terminal-wsl-distro-stopped-hold': distro }}
onStart={() => startHeldPanesForWslDistro(distro)}
distro={hold.distro}
unverifiable={hold.state === 'unverifiable'}
data={{
'data-terminal-wsl-distro-stopped-hold': hold.distro,
'data-terminal-wsl-distro-hold-state': hold.state
}}
onStart={() => startHeldPanesForWslDistro(hold.distro)}
/>
)
}
@@ -9,7 +9,7 @@ import type { ConnectPanePtySession } from './connect-pane-pty-session'
import type { HeldPaneReopenOutcome } from '../newer-daemon-hold-state'
import { holdForNewerDaemon } from './newer-daemon-hold-pane'
import { holdForStoppedWslDistro, wslRestoreStartArgs } from './wsl-distro-stopped-hold-pane'
import { parseWslDistroStoppedHold } from '../../../../../shared/wsl-distro-stopped-hold'
import { parseWslDistroHold } from '../../../../../shared/wsl-distro-stopped-hold'
import { NEWER_DAEMON_HOLD_ERROR } from '../../../../../shared/newer-daemon-hold'
import { PANE_NOT_LIVE_ERROR } from '../../../../../shared/pane-attach-only-view'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../../shared/layout-owner-switch'
@@ -91,7 +91,7 @@ function connectDeferredSessionReattach(
let paneOwnerUnverified = false
let paneOwnerChanged = false
let heldByNewerDaemon = false
let heldForStoppedDistro: string | null = null
let heldForStoppedDistro: ReturnType<typeof parseWslDistroHold> = null
// Why: main started this pane's agent; the window's attach never resumes it a second time.
const attachOnlyView = mainStartsPane(session)
const resumeStartup = session.buildColdRestoreAgentResumeStartup()
@@ -112,9 +112,7 @@ function connectDeferredSessionReattach(
heldByNewerDaemon = true
return
}
heldForStoppedDistro ??= session.runtimeEnvironmentId
? null
: parseWslDistroStoppedHold(message)
heldForStoppedDistro ??= session.runtimeEnvironmentId ? null : parseWslDistroHold(message)
if (heldForStoppedDistro) {
return
}
@@ -6,7 +6,7 @@ import {
noteUserWorkspaceSwitch,
resetWorkspaceSwitchUserActsForTests
} from '@/lib/workspace-switch-user-act'
import { wslDistroStoppedHoldMessage } from '../../../../../shared/wsl-distro-stopped-hold'
import { wslDistroHoldMessage } from '../../../../../shared/wsl-distro-stopped-hold'
import {
startHeldPanesForWslDistro,
wslDistroStoppedHoldStore
@@ -20,7 +20,7 @@ vi.mock('./pty-connect-limits', () => ({
isSshSessionGoneError: () => false
}))
const HOLD = `Error invoking remote method 'pty:spawn': Error: ${wslDistroStoppedHoldMessage('Ubuntu')}`
const HOLD = `Error invoking remote method 'pty:spawn': Error: ${wslDistroHoldMessage('Ubuntu', 'stopped')}`
function createSession(opts: { runtimeEnvironmentId?: string } = {}) {
let onError: (message: string) => void = () => {}
@@ -2,6 +2,7 @@ import { takeWorkspaceSwitchWake } from '@/lib/workspace-switch-user-act'
import { holdPaneForStoppedWslDistro } from '../wsl-distro-stopped-hold-state'
import { recordPtyConnectDiagnostic } from './pty-connect-limits'
import type { ConnectPanePtySession } from './connect-pane-pty-session'
import type { WslDistroHoldState } from '../../../../../shared/wsl-distro-stopped-hold'
// Clears the saved screen the hold painted so the restore below paints it once.
const CLEAR_HELD_SCREEN = '\x1b[0m\x1b[2J\x1b[3J\x1b[H'
@@ -34,10 +35,10 @@ export function wslRestoreStartArgs(
export function holdForStoppedWslDistro(
session: ConnectPanePtySession,
sessionId: string,
distro: string,
hold: { distro: string; state: WslDistroHoldState },
reconnect: () => Promise<unknown>
): void {
recordPtyConnectDiagnostic(`pane=${session.pane.id} -> HELD FOR STOPPED WSL ${distro}`)
recordPtyConnectDiagnostic(`pane=${session.pane.id} -> HELD FOR WSL ${hold.state} ${hold.distro}`)
const generation = session.transportStreamGeneration
const terminal = session.pane.terminal
const isCurrent = (): boolean =>
@@ -49,7 +50,7 @@ export function holdForStoppedWslDistro(
// Why: nothing backs this pane, so keystrokes have nowhere to go.
terminal.options.disableStdin = true
const forget = holdPaneForStoppedWslDistro(session.cacheKey, {
distro,
...hold,
release: async () => {
held = false
if (!isCurrent()) {
@@ -1,8 +1,11 @@
import { useStore } from 'zustand'
import { createStore } from 'zustand/vanilla'
import type { WslDistroHoldState } from '../../../../shared/wsl-distro-stopped-hold'
type HeldPane = {
distro: string
/** What the running read said: a stop, or no answer. */
state: WslDistroHoldState
/** Reconnects as the user's act, which may start the distro. */
release: () => Promise<unknown>
}
@@ -39,7 +42,11 @@ export function startHeldPanesForWslDistro(distro: string): void {
}
}
/** The stopped distro this pane is held for, or null. */
export function useWslDistroStoppedHold(paneKey: string): string | null {
return useStore(wslDistroStoppedHoldStore, (state) => state.panes[paneKey]?.distro ?? null)
/** The distro this pane is held for and what its read said, or null. */
export function useWslDistroStoppedHold(
paneKey: string
): { distro: string; state: WslDistroHoldState } | null {
const distro = useStore(wslDistroStoppedHoldStore, (store) => store.panes[paneKey]?.distro)
const state = useStore(wslDistroStoppedHoldStore, (store) => store.panes[paneKey]?.state)
return distro && state ? { distro, state } : null
}
+2 -1
View File
@@ -19305,7 +19305,8 @@
"reconnecting": "{{hostName}} stellt die Verbindung wieder her…",
"reconnect": "Erneut verbinden",
"stopped": "{{distro}} ist gestoppt",
"start": "Starten"
"start": "Starten",
"unverifiable": "Orca konnte nicht prüfen, ob {{distro}} läuft"
},
"interruptedContinue": {
"continue": "Fortfahren",
+2 -1
View File
@@ -19726,7 +19726,8 @@
"reconnecting": "{{hostName}} is reconnecting…",
"reconnect": "Reconnect",
"stopped": "{{distro}} is stopped",
"start": "Start"
"start": "Start",
"unverifiable": "Orca couldn't check whether {{distro}} is running"
},
"interruptedContinue": {
"continue": "Continue",
+4 -1
View File
@@ -19430,7 +19430,10 @@
"hostOutage": {
"offline": "{{hostName}} está sin conexión",
"reconnecting": "{{hostName}} se está reconectando…",
"reconnect": "Reconectar"
"reconnect": "Reconectar",
"stopped": "{{distro}} está detenido",
"start": "Iniciar",
"unverifiable": "Orca no pudo comprobar si {{distro}} se está ejecutando"
},
"interruptedContinue": {
"continue": "Continuar",
+4 -1
View File
@@ -19650,7 +19650,10 @@
"hostOutage": {
"offline": "{{hostName}} est hors ligne",
"reconnecting": "{{hostName}} se reconnecte…",
"reconnect": "Reconnecter"
"reconnect": "Reconnecter",
"stopped": "{{distro}} est arrêté",
"start": "Démarrer",
"unverifiable": "Orca n'a pas pu vérifier si {{distro}} est en cours d'exécution"
},
"interruptedContinue": {
"continue": "Continuer",
+4 -1
View File
@@ -18911,7 +18911,10 @@
"hostOutage": {
"offline": "{{hostName}} はオフラインです",
"reconnecting": "{{hostName}} に再接続しています…",
"reconnect": "再接続"
"reconnect": "再接続",
"stopped": "{{distro}} は停止しています",
"start": "起動",
"unverifiable": "Orca は {{distro}} が実行中かどうかを確認できませんでした"
},
"interruptedContinue": {
"continue": "続行",
+4 -1
View File
@@ -18911,7 +18911,10 @@
"hostOutage": {
"offline": "{{hostName}}이(가) 오프라인입니다",
"reconnecting": "{{hostName}}에 다시 연결하는 중…",
"reconnect": "다시 연결"
"reconnect": "다시 연결",
"stopped": "{{distro}}이(가) 중지되었습니다",
"start": "시작",
"unverifiable": "Orca가 {{distro}}이(가) 실행 중인지 확인하지 못했습니다"
},
"interruptedContinue": {
"continue": "계속",
+4 -1
View File
@@ -20215,7 +20215,10 @@
"hostOutage": {
"offline": "{{hostName}} está offline",
"reconnecting": "{{hostName}} está reconectando…",
"reconnect": "Reconectar"
"reconnect": "Reconectar",
"stopped": "{{distro}} está parado",
"start": "Iniciar",
"unverifiable": "O Orca não conseguiu verificar se {{distro}} está em execução"
},
"interruptedContinue": {
"continue": "Continuar",
+4 -1
View File
@@ -19903,7 +19903,10 @@
"hostOutage": {
"offline": "{{hostName}} не в сети",
"reconnecting": "Повторное подключение к {{hostName}}…",
"reconnect": "Подключиться снова"
"reconnect": "Подключиться снова",
"stopped": "{{distro}} остановлен",
"start": "Запустить",
"unverifiable": "Orca не удалось проверить, запущен ли {{distro}}"
},
"interruptedContinue": {
"continue": "Продолжить",
+4 -1
View File
@@ -19287,7 +19287,10 @@
"hostOutage": {
"offline": "{{hostName}} çevrimdışı",
"reconnecting": "{{hostName}} yeniden bağlanıyor…",
"reconnect": "Yeniden bağlan"
"reconnect": "Yeniden bağlan",
"stopped": "{{distro}} durduruldu",
"start": "Başlat",
"unverifiable": "Orca, {{distro}} öğesinin çalışıp çalışmadığını denetleyemedi"
},
"interruptedContinue": {
"continue": "Devam et",
+4 -1
View File
@@ -19028,7 +19028,10 @@
"hostOutage": {
"offline": "{{hostName}} 已離線",
"reconnecting": "正在重新連線 {{hostName}}…",
"reconnect": "重新連線"
"reconnect": "重新連線",
"stopped": "{{distro}} 已停止",
"start": "啟動",
"unverifiable": "Orca 無法確認 {{distro}} 是否正在執行"
},
"interruptedContinue": {
"continue": "繼續",
+4 -1
View File
@@ -18881,7 +18881,10 @@
"hostOutage": {
"offline": "{{hostName}} 已离线",
"reconnecting": "正在重新连接 {{hostName}}…",
"reconnect": "重新连接"
"reconnect": "重新连接",
"stopped": "{{distro}} 已停止",
"start": "启动",
"unverifiable": "Orca 无法确认 {{distro}} 是否正在运行"
},
"interruptedContinue": {
"continue": "继续",
+24 -11
View File
@@ -1,16 +1,29 @@
/** A passive restore of a WSL bridge pane whose distro is stopped: held, never started (E9). */
export const WSL_DISTRO_STOPPED_HOLD_ERROR = 'terminal_held_wsl_distro_stopped'
/**
* A passive restore of a WSL bridge pane whose distro is not proven running: held, never started
* (E9). The state says whether the read found it stopped or could not tell.
*/
export type WslDistroHoldState = 'stopped' | 'unverifiable'
export function wslDistroStoppedHoldMessage(distro: string): string {
return `${WSL_DISTRO_STOPPED_HOLD_ERROR}:${distro}`
const HOLD_ERRORS: Record<WslDistroHoldState, string> = {
stopped: 'terminal_held_wsl_distro_stopped',
unverifiable: 'terminal_held_wsl_distro_unverifiable'
}
/** The stopped distro a hold names, or null when the message is not one. */
export function parseWslDistroStoppedHold(message: string): string | null {
const start = message.indexOf(`${WSL_DISTRO_STOPPED_HOLD_ERROR}:`)
if (start === -1) {
return null
export function wslDistroHoldMessage(distro: string, state: WslDistroHoldState): string {
return `${HOLD_ERRORS[state]}:${distro}`
}
/** The distro and state a hold names, or null when the message is not one. */
export function parseWslDistroHold(
message: string
): { distro: string; state: WslDistroHoldState } | null {
for (const state of ['stopped', 'unverifiable'] as const) {
const prefix = `${HOLD_ERRORS[state]}:`
const start = message.indexOf(prefix)
const distro = start === -1 ? '' : message.slice(start + prefix.length).split(/\s/)[0]
if (distro) {
return { distro, state }
}
}
const distro = message.slice(start + WSL_DISTRO_STOPPED_HOLD_ERROR.length + 1).split(/\s/)[0]
return distro || null
return null
}