Files
u-desktop/updater/main.go
绝尘 f5a473c4b8 新增: 自动升级体系 + WorkerW重建检测 + 知识卡片优化
自动升级:
- 版本变量注入 (-ldflags -X main.version)
- 远程 version.json 检查 + 弹窗提示(非强制右下角/强制居中)
- updater.exe 嵌入主程序, 运行时释放到临时目录
- 下载+SHA256校验+备份+替换+回滚 全流程
- 托盘菜单"检查更新"

稳定性:
- WorkerW 重建自动检测(每10s)并重新嵌入
- 左键托盘点击防抖
- 设置窗口已打开时正确前置

优化:
- 知识卡片prompt改为面向开发者的硬核内容
- 配置目录统一到 ~/.u-desktop/
- 设置窗口WebView2数据目录改为configDir下
2026-05-28 19:06:26 +08:00

219 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"crypto/sha256"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func main() {
packageURL := flag.String("package-url", "", "下载地址")
sha256Hash := flag.String("sha256", "", "SHA256 校验值")
target := flag.String("target", "", "目标 exe 路径")
ver := flag.String("version", "", "目标版本号")
force := flag.Bool("force", false, "强制升级")
backupDir := flag.String("backup-dir", "", "备份目录")
flag.Parse()
logPath := filepath.Join(filepath.Dir(*target), "updater.log")
logFile, err := os.Create(logPath)
if err != nil {
fmt.Println("无法创建日志文件:", err)
os.Exit(1)
}
defer logFile.Close()
log.SetOutput(logFile)
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
log.Printf("updater 启动: version=%s target=%s force=%v", *ver, *target, *force)
if *packageURL == "" || *target == "" {
log.Fatal("缺少必要参数: --package-url, --target")
}
if *sha256Hash == "" {
log.Fatal("缺少 --sha256 参数")
}
// 等待主程序退出
time.Sleep(2 * time.Second)
// 下载
tmpFile, err := downloadWithRetry(*packageURL, 3)
if err != nil {
log.Fatal("下载失败: ", err)
}
defer os.Remove(tmpFile)
log.Println("下载完成:", tmpFile)
// 校验
if err := verifySHA256(tmpFile, *sha256Hash); err != nil {
log.Fatal("校验失败: ", err)
}
log.Println("SHA256 校验通过")
// 备份
if *backupDir == "" {
*backupDir = filepath.Join(filepath.Dir(*target), "backup")
}
os.MkdirAll(*backupDir, 0755)
backupPath := filepath.Join(*backupDir, fmt.Sprintf("old-%s.exe", time.Now().Format("20060102-150405")))
if err := copyFile(*target, backupPath); err != nil {
log.Println("备份失败(继续):", err)
} else {
log.Println("已备份:", backupPath)
}
// 替换: zip 包解压exe 直接覆盖
if strings.HasSuffix(strings.ToLower(*packageURL), ".zip") {
if err := extractAndReplace(tmpFile, *target); err != nil {
rollback(backupPath, *target)
log.Fatal("解压替换失败: ", err)
}
} else {
if err := copyFile(tmpFile, *target); err != nil {
rollback(backupPath, *target)
log.Fatal("替换失败: ", err)
}
}
log.Println("替换完成")
// 启动新版本
cmd := exec.Command(*target)
if err := cmd.Start(); err != nil {
rollback(backupPath, *target)
log.Fatal("启动新版本失败: ", err)
}
// 监听新进程 5 秒
time.Sleep(5 * time.Second)
if cmd.Process == nil {
rollback(backupPath, *target)
log.Fatal("新进程异常")
}
// 检查新进程是否仍在运行
// NOTE: Windows 下无法直接用 cmd.Process.Signals用简单的进程存活性判断
if err := cmd.Process.Release(); err != nil {
log.Println("新进程已退出,回滚")
rollback(backupPath, *target)
os.Exit(1)
}
log.Println("升级成功, updater 退出")
}
func downloadWithRetry(url string, maxRetries int) (string, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
if i > 0 {
log.Printf("重试下载 (%d/%d)...", i+1, maxRetries)
time.Sleep(5 * time.Second)
}
tmpFile, err := os.CreateTemp("", "updater-*.download")
if err != nil {
return "", fmt.Errorf("创建临时文件失败: %w", err)
}
tmpPath := tmpFile.Name()
resp, err := http.Get(url)
if err != nil {
tmpFile.Close()
os.Remove(tmpPath)
lastErr = err
continue
}
if resp.StatusCode != 200 {
tmpFile.Close()
os.Remove(tmpPath)
resp.Body.Close()
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
continue
}
_, err = io.Copy(tmpFile, resp.Body)
resp.Body.Close()
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
lastErr = err
continue
}
return tmpPath, nil
}
return "", fmt.Errorf("下载失败(重试 %d 次): %w", maxRetries, lastErr)
}
func verifySHA256(path, expected string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return err
}
actual := fmt.Sprintf("%x", h.Sum(nil))
if actual != strings.ToLower(expected) {
return fmt.Errorf("SHA256 不匹配: 期望 %s, 实际 %s", expected, actual)
}
return nil
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
// 复制可执行权限
if fi, err := os.Stat(src); err == nil {
out.Chmod(fi.Mode())
}
return nil
}
func extractAndReplace(zipPath, target string) error {
// TODO: 如需 zip 解压支持,引入 archive/zip
// 初期可直接分发 exe此路径不执行
return fmt.Errorf("zip 解压暂未实现,请使用 exe 分发")
}
func rollback(backupPath, target string) {
if backupPath == "" {
return
}
if _, err := os.Stat(backupPath); err != nil {
return
}
log.Println("回滚: 从备份恢复", backupPath)
if err := copyFile(backupPath, target); err != nil {
log.Println("回滚失败:", err)
return
}
cmd := exec.Command(target)
cmd.Start()
log.Println("已回滚并启动旧版本")
}