From 2f3976721182eecadd6c72c3364ea109208e8c60 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 5 Jul 2026 17:13:28 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(update):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=8B=E8=BD=BD=E8=BF=9B=E5=BA=A6=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=86=85=E9=83=A8=E9=94=AE=E5=B9=B6=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E9=80=9A=E9=81=93=E5=88=87=E6=8D=A2=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 下载进度状态拆分 key 与 version 字段,弹窗标题恢复显示纯版本号 - SetUpdateChannel 将检查、持久化与缓存清理并入同一临界区,消除下载窗口期竞态 - 通道配置文件改为临时文件加重命名的原子写入 - 移除 useAppUpdateManager 未使用的 isMacRuntime 参数 --- frontend/src/App.tsx | 1 - .../src/hooks/useAppUpdateManager.test.tsx | 9 ++++----- frontend/src/hooks/useAppUpdateManager.ts | 17 +++++++++------- internal/app/update_channel_state.go | 20 ++++++++++++++----- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 092a4b01..18b0b628 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1714,7 +1714,6 @@ function App() { updateChannel, updateDownloadProgress, } = useAppUpdateManager({ - isMacRuntime, runtimeBuildType, t, }); diff --git a/frontend/src/hooks/useAppUpdateManager.test.tsx b/frontend/src/hooks/useAppUpdateManager.test.tsx index 2d9703d8..1e17185c 100644 --- a/frontend/src/hooks/useAppUpdateManager.test.tsx +++ b/frontend/src/hooks/useAppUpdateManager.test.tsx @@ -54,10 +54,9 @@ describe('useAppUpdateManager', () => { return key; }; - const renderHook = (isMacRuntime: boolean) => { + const renderHook = () => { const Harness = () => { hook = useAppUpdateManager({ - isMacRuntime, runtimeBuildType: 'release', t, }); @@ -113,7 +112,7 @@ describe('useAppUpdateManager', () => { backendApp.InstallUpdateAndRestart.mockResolvedValue({ success: true }); backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true }); - renderHook(true); + renderHook(); await act(async () => { await hook?.checkForUpdates(false); @@ -146,7 +145,7 @@ describe('useAppUpdateManager', () => { }); backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true }); - renderHook(true); + renderHook(); await act(async () => { await hook?.checkForUpdates(false); @@ -173,7 +172,7 @@ describe('useAppUpdateManager', () => { }, }); - renderHook(false); + renderHook(); await act(async () => { await hook?.changeUpdateChannel('dev'); diff --git a/frontend/src/hooks/useAppUpdateManager.ts b/frontend/src/hooks/useAppUpdateManager.ts index 1c947cba..c09d4bbd 100644 --- a/frontend/src/hooks/useAppUpdateManager.ts +++ b/frontend/src/hooks/useAppUpdateManager.ts @@ -40,7 +40,6 @@ type UpdateDownloadResultData = { }; type UseAppUpdateManagerOptions = { - isMacRuntime: boolean; runtimeBuildType: string; t: Translator; }; @@ -48,6 +47,7 @@ type UseAppUpdateManagerOptions = { const createEmptyDownloadProgress = () => ({ open: false, version: '', + key: '', status: 'idle' as 'idle' | 'start' | 'downloading' | 'done' | 'error', percent: 0, downloaded: 0, @@ -64,7 +64,6 @@ const buildUpdateKey = (info: Pick | nu : ''; export const useAppUpdateManager = ({ - isMacRuntime: _isMacRuntime, runtimeBuildType, t, }: UseAppUpdateManagerOptions) => { @@ -148,7 +147,8 @@ export const useAppUpdateManager = ({ updateDownloadMetaRef.current = null; setUpdateDownloadProgress({ open: true, - version: targetKey, + version: info.latestVersion, + key: targetKey, status: 'start', percent: 0, downloaded: 0, @@ -219,7 +219,7 @@ export const useAppUpdateManager = ({ ); const isBackgroundProgressForLatestUpdate = Boolean(lastUpdateInfo?.hasUpdate) && Boolean(lastUpdateKey) - && updateDownloadProgress.version === lastUpdateKey + && updateDownloadProgress.key === lastUpdateKey && (updateDownloadProgress.status === 'start' || updateDownloadProgress.status === 'downloading' || updateDownloadProgress.status === 'done' @@ -292,8 +292,9 @@ export const useAppUpdateManager = ({ const total = info.assetSize || prev.total || 0; return { ...prev, - open: prev.open && prev.version === infoKey, - version: infoKey, + open: prev.open && prev.key === infoKey, + version: info.latestVersion, + key: infoKey, status: 'done', percent: 100, downloaded: total, @@ -317,7 +318,8 @@ export const useAppUpdateManager = ({ return { ...prev, open: false, - version: infoKey, + version: info.latestVersion, + key: infoKey, status: 'idle', percent: 0, downloaded: 0, @@ -485,6 +487,7 @@ export const useAppUpdateManager = ({ setUpdateDownloadProgress((prev) => ({ open: prev.open, version: prev.version, + key: prev.key, status: nextStatus, percent, downloaded, diff --git a/internal/app/update_channel_state.go b/internal/app/update_channel_state.go index d9b88517..de82feaf 100644 --- a/internal/app/update_channel_state.go +++ b/internal/app/update_channel_state.go @@ -81,7 +81,18 @@ func (a *App) persistUpdateChannel(channel updateChannel) error { if err != nil { return err } - return os.WriteFile(updateChannelMetadataPath(a.configDir), payload, 0o644) + + // 先写临时文件再重命名,避免进程中断留下损坏的 JSON。 + target := updateChannelMetadataPath(a.configDir) + tmp := target + ".tmp" + if err := os.WriteFile(tmp, payload, 0o644); err != nil { + return err + } + if err := os.Rename(tmp, target); err != nil { + _ = os.Remove(tmp) + return err + } + return nil } func (a *App) currentUpdateChannel() updateChannel { @@ -115,15 +126,16 @@ func (a *App) SetUpdateChannel(channel string) connection.QueryResult { return connection.QueryResult{Success: false, Message: a.localizedUpdateError(err)} } + // 检查、持久化与缓存清理必须在同一临界区内完成, + // 否则窗口期内启动的下载会把旧通道的安装包写回缓存。 a.updateMu.Lock() + defer a.updateMu.Unlock() if a.updateState.downloading { - a.updateMu.Unlock() return connection.QueryResult{ Success: false, Message: a.appText("app.update.backend.message.channel_change_blocked_downloading", nil), } } - a.updateMu.Unlock() if err := a.persistUpdateChannel(normalized); err != nil { logger.Error(err, "保存更新通道失败") @@ -133,10 +145,8 @@ func (a *App) SetUpdateChannel(channel string) connection.QueryResult { } } - a.updateMu.Lock() a.updateState.lastCheck = nil a.updateState.staged = nil - a.updateMu.Unlock() return connection.QueryResult{ Success: true,