From cfdd7d67202551af24fa2945febdc087ab32d8d4 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:04:09 -0700 Subject: [PATCH] fix(wsl): a held pane whose distro could not be checked says so, not that it stopped (#35860) --- ...y-adapter-wsl-passive-restore-hold.test.ts | 13 +++-- .../daemon/daemon-wsl-passive-restore-hold.ts | 7 +-- .../terminal-pane/WslDistroStoppedCard.tsx | 17 +++++-- .../WslDistroStoppedHoldCard.test.tsx | 50 +++++++++++++++++++ .../WslDistroStoppedHoldCard.tsx | 14 ++++-- .../deferred-session-reattach-connect.ts | 8 ++- ...n-reattach-wsl-distro-stopped-hold.test.ts | 4 +- .../wsl-distro-stopped-hold-pane.ts | 7 +-- .../wsl-distro-stopped-hold-state.ts | 13 +++-- src/renderer/src/i18n/locales/de.json | 3 +- src/renderer/src/i18n/locales/en.json | 3 +- src/renderer/src/i18n/locales/es.json | 5 +- src/renderer/src/i18n/locales/fr.json | 5 +- src/renderer/src/i18n/locales/ja.json | 5 +- src/renderer/src/i18n/locales/ko.json | 5 +- src/renderer/src/i18n/locales/pt.json | 5 +- src/renderer/src/i18n/locales/ru.json | 5 +- src/renderer/src/i18n/locales/tr.json | 5 +- src/renderer/src/i18n/locales/zh-Hant.json | 5 +- src/renderer/src/i18n/locales/zh.json | 5 +- src/shared/wsl-distro-stopped-hold.ts | 35 +++++++++---- 21 files changed, 170 insertions(+), 49 deletions(-) create mode 100644 src/renderer/src/components/terminal-pane/WslDistroStoppedHoldCard.test.tsx diff --git a/src/main/daemon/daemon-pty-adapter-wsl-passive-restore-hold.test.ts b/src/main/daemon/daemon-pty-adapter-wsl-passive-restore-hold.test.ts index d80255e5d93..813957e68a2 100644 --- a/src/main/daemon/daemon-pty-adapter-wsl-passive-restore-hold.test.ts +++ b/src/main/daemon/daemon-pty-adapter-wsl-passive-restore-hold.test.ts @@ -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) }) diff --git a/src/main/daemon/daemon-wsl-passive-restore-hold.ts b/src/main/daemon/daemon-wsl-passive-restore-hold.ts index 40567f6c99f..8a67361c1ca 100644 --- a/src/main/daemon/daemon-wsl-passive-restore-hold.ts +++ b/src/main/daemon/daemon-wsl-passive-restore-hold.ts @@ -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)) } } diff --git a/src/renderer/src/components/terminal-pane/WslDistroStoppedCard.tsx b/src/renderer/src/components/terminal-pane/WslDistroStoppedCard.tsx index 13cefdce5a8..99b154f3e82 100644 --- a/src/renderer/src/components/terminal-pane/WslDistroStoppedCard.tsx +++ b/src/renderer/src/components/terminal-pane/WslDistroStoppedCard.tsx @@ -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({ ({ + translate: (_key: string, fallback: string, options?: Record) => + 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() + 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() + }) +}) diff --git a/src/renderer/src/components/terminal-pane/WslDistroStoppedHoldCard.tsx b/src/renderer/src/components/terminal-pane/WslDistroStoppedHoldCard.tsx index 37761661633..a091cc579c6 100644 --- a/src/renderer/src/components/terminal-pane/WslDistroStoppedHoldCard.tsx +++ b/src/renderer/src/components/terminal-pane/WslDistroStoppedHoldCard.tsx @@ -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 ( 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)} /> ) } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts index 0f765ae4190..02817f70e15 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-connect.ts @@ -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 = 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 } diff --git a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-wsl-distro-stopped-hold.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-wsl-distro-stopped-hold.test.ts index 06300a9aaaf..177f4b29bfc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-wsl-distro-stopped-hold.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-wsl-distro-stopped-hold.test.ts @@ -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 = () => {} diff --git a/src/renderer/src/components/terminal-pane/pty-connection/wsl-distro-stopped-hold-pane.ts b/src/renderer/src/components/terminal-pane/pty-connection/wsl-distro-stopped-hold-pane.ts index e027b2ff3a8..7ce56c01e94 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/wsl-distro-stopped-hold-pane.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/wsl-distro-stopped-hold-pane.ts @@ -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 ): 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()) { diff --git a/src/renderer/src/components/terminal-pane/wsl-distro-stopped-hold-state.ts b/src/renderer/src/components/terminal-pane/wsl-distro-stopped-hold-state.ts index 2e8eca0e54d..3670e9f8b78 100644 --- a/src/renderer/src/components/terminal-pane/wsl-distro-stopped-hold-state.ts +++ b/src/renderer/src/components/terminal-pane/wsl-distro-stopped-hold-state.ts @@ -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 } @@ -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 } diff --git a/src/renderer/src/i18n/locales/de.json b/src/renderer/src/i18n/locales/de.json index c1ea10632b6..c8eba94e72c 100644 --- a/src/renderer/src/i18n/locales/de.json +++ b/src/renderer/src/i18n/locales/de.json @@ -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", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index f5d83eaf8d7..11cf862a6d1 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index a26be847c75..65c75af81a0 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index c184c3de042..2cf386e4949 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index f75449a9228..64aee77f870 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -18911,7 +18911,10 @@ "hostOutage": { "offline": "{{hostName}} はオフラインです", "reconnecting": "{{hostName}} に再接続しています…", - "reconnect": "再接続" + "reconnect": "再接続", + "stopped": "{{distro}} は停止しています", + "start": "起動", + "unverifiable": "Orca は {{distro}} が実行中かどうかを確認できませんでした" }, "interruptedContinue": { "continue": "続行", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 3aabe8d0d60..73171f056ec 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -18911,7 +18911,10 @@ "hostOutage": { "offline": "{{hostName}}이(가) 오프라인입니다", "reconnecting": "{{hostName}}에 다시 연결하는 중…", - "reconnect": "다시 연결" + "reconnect": "다시 연결", + "stopped": "{{distro}}이(가) 중지되었습니다", + "start": "시작", + "unverifiable": "Orca가 {{distro}}이(가) 실행 중인지 확인하지 못했습니다" }, "interruptedContinue": { "continue": "계속", diff --git a/src/renderer/src/i18n/locales/pt.json b/src/renderer/src/i18n/locales/pt.json index e180b54407b..ecbc2d3c4a5 100644 --- a/src/renderer/src/i18n/locales/pt.json +++ b/src/renderer/src/i18n/locales/pt.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ru.json b/src/renderer/src/i18n/locales/ru.json index 2e02398fd55..02cfc042bee 100644 --- a/src/renderer/src/i18n/locales/ru.json +++ b/src/renderer/src/i18n/locales/ru.json @@ -19903,7 +19903,10 @@ "hostOutage": { "offline": "{{hostName}} не в сети", "reconnecting": "Повторное подключение к {{hostName}}…", - "reconnect": "Подключиться снова" + "reconnect": "Подключиться снова", + "stopped": "{{distro}} остановлен", + "start": "Запустить", + "unverifiable": "Orca не удалось проверить, запущен ли {{distro}}" }, "interruptedContinue": { "continue": "Продолжить", diff --git a/src/renderer/src/i18n/locales/tr.json b/src/renderer/src/i18n/locales/tr.json index c6f99ed30f2..6b6b04697a1 100644 --- a/src/renderer/src/i18n/locales/tr.json +++ b/src/renderer/src/i18n/locales/tr.json @@ -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", diff --git a/src/renderer/src/i18n/locales/zh-Hant.json b/src/renderer/src/i18n/locales/zh-Hant.json index fa5baab5aa8..abc9ec46b89 100644 --- a/src/renderer/src/i18n/locales/zh-Hant.json +++ b/src/renderer/src/i18n/locales/zh-Hant.json @@ -19028,7 +19028,10 @@ "hostOutage": { "offline": "{{hostName}} 已離線", "reconnecting": "正在重新連線 {{hostName}}…", - "reconnect": "重新連線" + "reconnect": "重新連線", + "stopped": "{{distro}} 已停止", + "start": "啟動", + "unverifiable": "Orca 無法確認 {{distro}} 是否正在執行" }, "interruptedContinue": { "continue": "繼續", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 98be6bf4550..1a62ba00b2f 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -18881,7 +18881,10 @@ "hostOutage": { "offline": "{{hostName}} 已离线", "reconnecting": "正在重新连接 {{hostName}}…", - "reconnect": "重新连接" + "reconnect": "重新连接", + "stopped": "{{distro}} 已停止", + "start": "启动", + "unverifiable": "Orca 无法确认 {{distro}} 是否正在运行" }, "interruptedContinue": { "continue": "继续", diff --git a/src/shared/wsl-distro-stopped-hold.ts b/src/shared/wsl-distro-stopped-hold.ts index 3c8e0a222cd..bd28d0857af 100644 --- a/src/shared/wsl-distro-stopped-hold.ts +++ b/src/shared/wsl-distro-stopped-hold.ts @@ -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 = { + 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 }