自动升级: - 版本变量注入 (-ldflags -X main.version) - 远程 version.json 检查 + 弹窗提示(非强制右下角/强制居中) - updater.exe 嵌入主程序, 运行时释放到临时目录 - 下载+SHA256校验+备份+替换+回滚 全流程 - 托盘菜单"检查更新" 稳定性: - WorkerW 重建自动检测(每10s)并重新嵌入 - 左键托盘点击防抖 - 设置窗口已打开时正确前置 优化: - 知识卡片prompt改为面向开发者的硬核内容 - 配置目录统一到 ~/.u-desktop/ - 设置窗口WebView2数据目录改为configDir下
272 lines
5.7 KiB
Go
272 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jchv/go-webview2"
|
|
)
|
|
|
|
//go:embed web/update.html
|
|
var updateHTML string
|
|
|
|
//go:embed updater.exe
|
|
var updaterExe []byte
|
|
|
|
type PackageInfo struct {
|
|
URL string `json:"url"`
|
|
Size int64 `json:"size"`
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
|
|
type UpdateInfo struct {
|
|
Version string `json:"version"`
|
|
Force bool `json:"force"`
|
|
ReleaseNotes string `json:"release_notes"`
|
|
Package PackageInfo `json:"package"`
|
|
}
|
|
|
|
var (
|
|
updateMu sync.Mutex
|
|
updateOpen bool
|
|
updateInfo *UpdateInfo
|
|
)
|
|
|
|
const defaultUpdateURL = "https://c.1216.top/u-desktop/version.json"
|
|
|
|
func getUpdateURL() string {
|
|
cfg := loadConfig()
|
|
if cfg.UpdateURL != "" {
|
|
return cfg.UpdateURL
|
|
}
|
|
return defaultUpdateURL
|
|
}
|
|
|
|
func checkUpdate() (*UpdateInfo, error) {
|
|
resp, err := httpClient.Get(getUpdateURL())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("请求 version.json 失败: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("version.json 返回状态 %d", resp.StatusCode)
|
|
}
|
|
|
|
var info UpdateInfo
|
|
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
|
return nil, fmt.Errorf("解析 version.json 失败: %w", err)
|
|
}
|
|
|
|
if info.Version == "" || info.Version == version || info.Version == "dev" {
|
|
return nil, nil
|
|
}
|
|
|
|
// 用户跳过过此版本且非强制
|
|
cfg := loadConfig()
|
|
if !info.Force && cfg.SkipVersion == info.Version {
|
|
return nil, nil
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
func runAutoUpdateCheck() {
|
|
cfg := loadConfig()
|
|
if !cfg.AutoCheckUpdate {
|
|
return
|
|
}
|
|
time.Sleep(3 * time.Second)
|
|
|
|
info, err := checkUpdate()
|
|
if err != nil {
|
|
log.Println("自动更新检查失败:", err)
|
|
return
|
|
}
|
|
if info != nil {
|
|
showUpdateWindow(info)
|
|
}
|
|
}
|
|
|
|
func showUpdateWindow(info *UpdateInfo) {
|
|
updateMu.Lock()
|
|
if updateOpen {
|
|
updateMu.Unlock()
|
|
return
|
|
}
|
|
updateOpen = true
|
|
updateInfo = info
|
|
updateMu.Unlock()
|
|
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
defer func() {
|
|
updateMu.Lock()
|
|
updateOpen = false
|
|
updateInfo = nil
|
|
updateMu.Unlock()
|
|
runtime.UnlockOSThread()
|
|
}()
|
|
|
|
winW := 340
|
|
winH := 220
|
|
if info.Force {
|
|
winW = 420
|
|
winH = 260
|
|
}
|
|
|
|
dataDir := filepath.Join(configDir(), "update-data")
|
|
os.MkdirAll(dataDir, 0755)
|
|
|
|
w := webview2.NewWithOptions(webview2.WebViewOptions{
|
|
AutoFocus: true,
|
|
DataPath: dataDir,
|
|
WindowOptions: webview2.WindowOptions{
|
|
Title: "更新提示",
|
|
Width: uint(winW),
|
|
Height: uint(winH),
|
|
},
|
|
})
|
|
if w == nil {
|
|
log.Println("更新窗口: 创建失败")
|
|
return
|
|
}
|
|
|
|
hwnd := uintptr(w.Window())
|
|
|
|
// 无边框
|
|
style, _, _ := procGetWindowLongPtrW.Call(hwnd, gwlStyle)
|
|
procSetWindowLongPtrW.Call(hwnd, gwlStyle, style & ^uintptr(wsSizebox|wsMaxbox|0x00C00000))
|
|
// 置顶 + 工具窗口
|
|
procSetWindowLongPtrW.Call(hwnd, gwlExStyle, wsExToolwindow|0x00000008)
|
|
|
|
// 定位窗口
|
|
screenW, screenH := getScreenSize()
|
|
if info.Force {
|
|
x := (int(screenW) - winW) / 2
|
|
y := (int(screenH) - winH) / 2
|
|
procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(winW), uintptr(winH), 1)
|
|
} else {
|
|
x := int(screenW) - winW - 20
|
|
y := int(screenH) - winH - 60
|
|
procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(winW), uintptr(winH), 1)
|
|
}
|
|
|
|
w.Bind("getUpdateInfo", func() string {
|
|
if updateInfo == nil {
|
|
return "{}"
|
|
}
|
|
data, _ := json.Marshal(map[string]interface{}{
|
|
"version": updateInfo.Version,
|
|
"force": updateInfo.Force,
|
|
"releaseNotes": updateInfo.ReleaseNotes,
|
|
"size": updateInfo.Package.Size,
|
|
"currentVersion": version,
|
|
})
|
|
return string(data)
|
|
})
|
|
|
|
w.Bind("skipUpdate", func() string {
|
|
if updateInfo != nil && !updateInfo.Force {
|
|
cfg := loadConfig()
|
|
cfg.SkipVersion = updateInfo.Version
|
|
saveConfig(cfg)
|
|
}
|
|
return ""
|
|
})
|
|
|
|
w.Bind("startUpdate", func() string {
|
|
if updateInfo == nil {
|
|
return ""
|
|
}
|
|
go doStartUpdate(updateInfo)
|
|
return ""
|
|
})
|
|
|
|
w.SetHtml(updateHTML)
|
|
log.Println("更新窗口已打开")
|
|
w.Run()
|
|
log.Println("更新窗口已关闭")
|
|
}()
|
|
}
|
|
|
|
func doStartUpdate(info *UpdateInfo) {
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
log.Println("获取自身路径失败:", err)
|
|
return
|
|
}
|
|
exeDir := filepath.Dir(exePath)
|
|
|
|
// 释放嵌入的 updater.exe 到临时目录
|
|
updaterPath := filepath.Join(os.TempDir(), "u-desktop-updater.exe")
|
|
if err := os.WriteFile(updaterPath, updaterExe, 0755); err != nil {
|
|
log.Println("释放 updater.exe 失败:", err)
|
|
showError("释放升级器失败: " + err.Error())
|
|
return
|
|
}
|
|
|
|
backupDir := filepath.Join(exeDir, "backup")
|
|
os.MkdirAll(backupDir, 0755)
|
|
|
|
args := []string{
|
|
"--package-url", info.Package.URL,
|
|
"--sha256", info.Package.SHA256,
|
|
"--target", exePath,
|
|
"--version", info.Version,
|
|
"--backup-dir", backupDir,
|
|
}
|
|
if info.Force {
|
|
args = append(args, "--force")
|
|
}
|
|
|
|
cmd := exec.Command(updaterPath, args...)
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
if err := cmd.Start(); err != nil {
|
|
log.Println("启动 updater 失败:", err)
|
|
return
|
|
}
|
|
|
|
log.Println("updater 已启动, 主程序退出")
|
|
os.Exit(0)
|
|
}
|
|
|
|
func manualCheckUpdate() {
|
|
info, err := checkUpdate()
|
|
if err != nil {
|
|
log.Println("手动更新检查失败:", err)
|
|
showError("检查更新失败: " + err.Error())
|
|
return
|
|
}
|
|
if info == nil {
|
|
showError("当前已是最新版本 (" + version + ")")
|
|
return
|
|
}
|
|
showUpdateWindow(info)
|
|
}
|
|
|
|
func formatSize(size int64) string {
|
|
const (
|
|
KB = 1024
|
|
MB = KB * 1024
|
|
)
|
|
switch {
|
|
case size >= MB:
|
|
return strconv.FormatFloat(float64(size)/float64(MB), 'f', 1, 64) + " MB"
|
|
case size >= KB:
|
|
return strconv.FormatFloat(float64(size)/float64(KB), 'f', 1, 64) + " KB"
|
|
default:
|
|
return strconv.FormatInt(size, 10) + " B"
|
|
}
|
|
}
|