mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
fix(wsl): a held pane whose distro could not be checked says so, not that it stopped (#35860)
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+3
-5
@@ -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
|
||||
}
|
||||
|
||||
+2
-2
@@ -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 = () => {}
|
||||
|
||||
+4
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -18911,7 +18911,10 @@
|
||||
"hostOutage": {
|
||||
"offline": "{{hostName}} はオフラインです",
|
||||
"reconnecting": "{{hostName}} に再接続しています…",
|
||||
"reconnect": "再接続"
|
||||
"reconnect": "再接続",
|
||||
"stopped": "{{distro}} は停止しています",
|
||||
"start": "起動",
|
||||
"unverifiable": "Orca は {{distro}} が実行中かどうかを確認できませんでした"
|
||||
},
|
||||
"interruptedContinue": {
|
||||
"continue": "続行",
|
||||
|
||||
@@ -18911,7 +18911,10 @@
|
||||
"hostOutage": {
|
||||
"offline": "{{hostName}}이(가) 오프라인입니다",
|
||||
"reconnecting": "{{hostName}}에 다시 연결하는 중…",
|
||||
"reconnect": "다시 연결"
|
||||
"reconnect": "다시 연결",
|
||||
"stopped": "{{distro}}이(가) 중지되었습니다",
|
||||
"start": "시작",
|
||||
"unverifiable": "Orca가 {{distro}}이(가) 실행 중인지 확인하지 못했습니다"
|
||||
},
|
||||
"interruptedContinue": {
|
||||
"continue": "계속",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19903,7 +19903,10 @@
|
||||
"hostOutage": {
|
||||
"offline": "{{hostName}} не в сети",
|
||||
"reconnecting": "Повторное подключение к {{hostName}}…",
|
||||
"reconnect": "Подключиться снова"
|
||||
"reconnect": "Подключиться снова",
|
||||
"stopped": "{{distro}} остановлен",
|
||||
"start": "Запустить",
|
||||
"unverifiable": "Orca не удалось проверить, запущен ли {{distro}}"
|
||||
},
|
||||
"interruptedContinue": {
|
||||
"continue": "Продолжить",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -19028,7 +19028,10 @@
|
||||
"hostOutage": {
|
||||
"offline": "{{hostName}} 已離線",
|
||||
"reconnecting": "正在重新連線 {{hostName}}…",
|
||||
"reconnect": "重新連線"
|
||||
"reconnect": "重新連線",
|
||||
"stopped": "{{distro}} 已停止",
|
||||
"start": "啟動",
|
||||
"unverifiable": "Orca 無法確認 {{distro}} 是否正在執行"
|
||||
},
|
||||
"interruptedContinue": {
|
||||
"continue": "繼續",
|
||||
|
||||
@@ -18881,7 +18881,10 @@
|
||||
"hostOutage": {
|
||||
"offline": "{{hostName}} 已离线",
|
||||
"reconnecting": "正在重新连接 {{hostName}}…",
|
||||
"reconnect": "重新连接"
|
||||
"reconnect": "重新连接",
|
||||
"stopped": "{{distro}} 已停止",
|
||||
"start": "启动",
|
||||
"unverifiable": "Orca 无法确认 {{distro}} 是否正在运行"
|
||||
},
|
||||
"interruptedContinue": {
|
||||
"continue": "继续",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user