Files
MyGoNavi/internal/app/update_channel_state.go
Syngnat 2f39767211 🐛 fix(update): 修复下载进度标题显示内部键并消除通道切换竞态
- 下载进度状态拆分 key 与 version 字段,弹窗标题恢复显示纯版本号
- SetUpdateChannel 将检查、持久化与缓存清理并入同一临界区,消除下载窗口期竞态
- 通道配置文件改为临时文件加重命名的原子写入
- 移除 useAppUpdateManager 未使用的 isMacRuntime 参数
2026-07-05 17:13:28 +08:00

159 lines
3.9 KiB
Go

package app
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/logger"
)
const (
updateChannelFileName = "update_channel.json"
updateDevReleaseTag = "dev-latest"
)
type updateChannel string
const (
updateChannelLatest updateChannel = "latest"
updateChannelDev updateChannel = "dev"
)
type updateChannelStateFile struct {
Channel updateChannel `json:"channel"`
}
func normalizeUpdateChannel(value string) (updateChannel, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", string(updateChannelLatest):
return updateChannelLatest, nil
case string(updateChannelDev):
return updateChannelDev, nil
default:
return "", localizedUpdateError{
key: "app.update.backend.error.channel_invalid",
params: map[string]any{"channel": strings.TrimSpace(value)},
}
}
}
func defaultUpdateChannel() updateChannel {
version := strings.ToLower(strings.TrimSpace(getCurrentVersion()))
if strings.HasPrefix(version, "dev-") {
return updateChannelDev
}
return updateChannelLatest
}
func updateChannelMetadataPath(configDir string) string {
return filepath.Join(configDir, updateChannelFileName)
}
func (a *App) loadStoredUpdateChannel() (updateChannel, error) {
if strings.TrimSpace(a.configDir) == "" {
a.configDir = resolveAppConfigDir()
}
data, err := os.ReadFile(updateChannelMetadataPath(a.configDir))
if err != nil {
return "", err
}
var state updateChannelStateFile
if err := json.Unmarshal(data, &state); err != nil {
return "", err
}
return normalizeUpdateChannel(string(state.Channel))
}
func (a *App) persistUpdateChannel(channel updateChannel) error {
if strings.TrimSpace(a.configDir) == "" {
a.configDir = resolveAppConfigDir()
}
if err := os.MkdirAll(a.configDir, 0o755); err != nil {
return err
}
payload, err := json.MarshalIndent(updateChannelStateFile{Channel: channel}, "", " ")
if err != nil {
return err
}
// 先写临时文件再重命名,避免进程中断留下损坏的 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 {
if a == nil {
return defaultUpdateChannel()
}
channel, err := a.loadStoredUpdateChannel()
if err == nil {
return channel
}
if !os.IsNotExist(err) {
logger.Error(err, "加载更新通道配置失败")
}
return defaultUpdateChannel()
}
func (a *App) GetUpdateChannel() connection.QueryResult {
channel := a.currentUpdateChannel()
return connection.QueryResult{
Success: true,
Message: "OK",
Data: map[string]any{
"channel": string(channel),
},
}
}
func (a *App) SetUpdateChannel(channel string) connection.QueryResult {
normalized, err := normalizeUpdateChannel(channel)
if err != nil {
return connection.QueryResult{Success: false, Message: a.localizedUpdateError(err)}
}
// 检查、持久化与缓存清理必须在同一临界区内完成,
// 否则窗口期内启动的下载会把旧通道的安装包写回缓存。
a.updateMu.Lock()
defer a.updateMu.Unlock()
if a.updateState.downloading {
return connection.QueryResult{
Success: false,
Message: a.appText("app.update.backend.message.channel_change_blocked_downloading", nil),
}
}
if err := a.persistUpdateChannel(normalized); err != nil {
logger.Error(err, "保存更新通道失败")
return connection.QueryResult{
Success: false,
Message: a.appText("app.update.backend.message.channel_change_failed", map[string]any{"detail": err.Error()}),
}
}
a.updateState.lastCheck = nil
a.updateState.staged = nil
return connection.QueryResult{
Success: true,
Message: a.appText("app.update.backend.message.channel_changed", map[string]any{"channel": string(normalized)}),
Data: map[string]any{
"channel": string(normalized),
},
}
}