新增: 自动升级体系 + WorkerW重建检测 + 知识卡片优化
自动升级: - 版本变量注入 (-ldflags -X main.version) - 远程 version.json 检查 + 弹窗提示(非强制右下角/强制居中) - updater.exe 嵌入主程序, 运行时释放到临时目录 - 下载+SHA256校验+备份+替换+回滚 全流程 - 托盘菜单"检查更新" 稳定性: - WorkerW 重建自动检测(每10s)并重新嵌入 - 左键托盘点击防抖 - 设置窗口已打开时正确前置 优化: - 知识卡片prompt改为面向开发者的硬核内容 - 配置目录统一到 ~/.u-desktop/ - 设置窗口WebView2数据目录改为configDir下
This commit is contained in:
218
updater/main.go
Normal file
218
updater/main.go
Normal file
@@ -0,0 +1,218 @@
|
||||
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("已回滚并启动旧版本")
|
||||
}
|
||||
Reference in New Issue
Block a user