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({