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,