🐛 fix(update): 修复下载进度标题显示内部键并消除通道切换竞态

- 下载进度状态拆分 key 与 version 字段,弹窗标题恢复显示纯版本号
- SetUpdateChannel 将检查、持久化与缓存清理并入同一临界区,消除下载窗口期竞态
- 通道配置文件改为临时文件加重命名的原子写入
- 移除 useAppUpdateManager 未使用的 isMacRuntime 参数
This commit is contained in:
Syngnat
2026-07-05 17:13:28 +08:00
parent 5beb4c3ce4
commit 2f39767211
4 changed files with 29 additions and 18 deletions

View File

@@ -1714,7 +1714,6 @@ function App() {
updateChannel,
updateDownloadProgress,
} = useAppUpdateManager({
isMacRuntime,
runtimeBuildType,
t,
});

View File

@@ -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');

View File

@@ -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<UpdateInfo, 'channel' | 'latestVersion'> | 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,

View File

@@ -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,