diff --git a/apps/desktop/src/app/updates-overlay.tsx b/apps/desktop/src/app/updates-overlay.tsx index d55f2d79c13..4bf47410d86 100644 --- a/apps/desktop/src/app/updates-overlay.tsx +++ b/apps/desktop/src/app/updates-overlay.tsx @@ -189,7 +189,7 @@ function IdleView({ if (behind === 0) { return ( } title={u.allSetTitle} /> diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index b02fde46909..1f1d511e971 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1237,6 +1237,7 @@ export const en: Translations = { unsupportedMessage: 'This version of Hermes can’t update itself from inside the app.', connectionRetry: 'Check your connection and try again.', latestBody: 'You’re running the latest version.', + latestBodyBackend: 'The backend is running the latest version.', allSetTitle: 'You’re all set', availableTitle: 'New update available', availableBody: 'A new version of Hermes is ready to install.', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index afaf629a884..b1c944f30e1 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -1378,6 +1378,7 @@ export const ja = defineLocale({ unsupportedMessage: 'このバージョンの Hermes はアプリ内から自分を更新できません。', connectionRetry: '接続を確認してもう一度試してください。', latestBody: '最新バージョンを実行しています。', + latestBodyBackend: 'バックエンドは最新バージョンを実行しています。', allSetTitle: '準備完了', availableTitle: '新しい更新が利用可能', availableBody: '新しいバージョンの Hermes をインストールする準備ができています。', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index c32fbae451b..bc442951e37 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -937,6 +937,7 @@ export interface Translations { unsupportedMessage: string connectionRetry: string latestBody: string + latestBodyBackend: string allSetTitle: string availableTitle: string availableBody: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 8804f36cfd9..c7b2018fd3e 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -1344,6 +1344,7 @@ export const zhHant = defineLocale({ unsupportedMessage: '此版本的 Hermes 無法在應用程式內自行更新。', connectionRetry: '請檢查網路連線後重試。', latestBody: '您正在執行最新版本。', + latestBodyBackend: '後端正在執行最新版本。', allSetTitle: '已是最新版本', availableTitle: '有可用更新', availableBody: '新版 Hermes 已可安裝。', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index e3309af7c25..27c0b1840bc 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -1424,6 +1424,7 @@ export const zh: Translations = { unsupportedMessage: '此版本的 Hermes 无法在应用内自行更新。', connectionRetry: '请检查网络连接后重试。', latestBody: '你正在运行最新版本。', + latestBodyBackend: '后端正在运行最新版本。', allSetTitle: '已是最新', availableTitle: '有可用更新', availableBody: '新版 Hermes 已可安装。', diff --git a/apps/desktop/src/store/updates.test.ts b/apps/desktop/src/store/updates.test.ts index 3aac4004c3c..01f78bc08dc 100644 --- a/apps/desktop/src/store/updates.test.ts +++ b/apps/desktop/src/store/updates.test.ts @@ -182,5 +182,18 @@ describe('applyBackendUpdate recovery', () => { expect($backendUpdateApply.get().stage).toBe('idle') expect($backendUpdateApply.get().applying).toBe(false) }) + + it('surfaces an error when the backend never comes back after the restart', async () => { + updateHermesSpy.mockResolvedValue({ ok: true, name: 'update', pid: 1 }) + getActionStatusSpy.mockRejectedValue(new Error('ECONNREFUSED')) + checkHermesUpdateSpy.mockRejectedValue(new Error('ECONNREFUSED')) + + const promise = applyBackendUpdate() + await vi.advanceTimersByTimeAsync(70000) + const result = await promise + + expect(result.ok).toBe(false) + expect($backendUpdateApply.get().stage).toBe('error') + }) }) diff --git a/apps/desktop/src/store/updates.ts b/apps/desktop/src/store/updates.ts index c31e3e9d9f4..cc64f4822f6 100644 --- a/apps/desktop/src/store/updates.ts +++ b/apps/desktop/src/store/updates.ts @@ -307,17 +307,39 @@ export async function applyUpdates(opts: DesktopUpdateApplyOptions = {}): Promis const BACKEND_RETURN_POLL_MS = 1500 const BACKEND_RETURN_MAX_ATTEMPTS = 40 -async function waitForBackendReturn(): Promise { +async function waitForBackendReturn(): Promise { for (let attempt = 0; attempt < BACKEND_RETURN_MAX_ATTEMPTS; attempt += 1) { await new Promise(resolve => globalThis.setTimeout(resolve, BACKEND_RETURN_POLL_MS)) try { await checkHermesUpdate() - return + return true } catch { continue } } + + return false +} + +function finishBackendApply(returned: boolean): DesktopUpdateApplyResult { + if (returned) { + $backendUpdateApply.set(IDLE) + setUpdateOverlayOpen(false) + void checkBackendUpdates() + + return { ok: true, message: 'Backend update applied.' } + } + + $backendUpdateApply.set({ + ...$backendUpdateApply.get(), + applying: false, + stage: 'error', + error: 'apply-failed', + message: 'Backend updated but did not come back online. Check the backend host.' + }) + + return { ok: false, error: 'apply-failed', message: 'Backend did not come back online.' } } export async function applyBackendUpdate(): Promise { @@ -350,11 +372,8 @@ export async function applyBackendUpdate(): Promise { stage: 'restart', message: 'Backend restarting to load the update…' }) - await waitForBackendReturn() - $backendUpdateApply.set(IDLE) - void checkBackendUpdates() - return { ok: true, message: 'Backend update applied; backend is back online.' } + return finishBackendApply(await waitForBackendReturn()) } if (last && !last.running) { @@ -365,11 +384,8 @@ export async function applyBackendUpdate(): Promise { const ok = !!last && (last.exit_code ?? 1) === 0 if (ok) { $backendUpdateApply.set({ ...$backendUpdateApply.get(), applying: true, stage: 'restart', message: 'Backend restarting to load the update…' }) - await waitForBackendReturn() - $backendUpdateApply.set(IDLE) - void checkBackendUpdates() - return { ok: true, message: 'Backend update applied.' } + return finishBackendApply(await waitForBackendReturn()) } $backendUpdateApply.set({