完善自我更新逻辑

This commit is contained in:
BBIT-Kai
2025-12-30 17:54:33 +08:00
parent ec29d883bd
commit 62e8ecb7d6
12 changed files with 248 additions and 112 deletions
+108 -42
View File
@@ -1,83 +1,149 @@
package main
import (
"crypto/sha256"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"sentinel/pkg/log"
"sentinel/pkg/utils"
"runtime"
"sentinel/pkg/device"
"sentinel/pkg/log"
"sentinel/pkg/net"
"strconv"
"syscall"
"time"
)
func main() {
deviceID := device.GetDeviceID()
fmt.Printf("[updater] device id: %s\n", deviceID)
// 定义命令行参数
version := flag.String("version", "", "current version of main program")
exeDir, _ := os.Executable()
target := filepath.Join(filepath.Dir(exeDir), "main_program_binary_name") // TODO: 替换
flag.Parse()
if err := RunUpdate(deviceID, target); err != nil {
log.Fatalf("[updater] update failed: %v", err)
if *version == "" {
// updater 视角:-1 表示“未知版本”,一定触发更新检测
*version = "0"
log.Println("[updater] --version not provided, fallback to -1")
fmt.Println("[updater] 主程序版本号:", *version)
}
fmt.Println("[updater] update finished")
}
// RunUpdate 检查更新、下载、替换主程序并启动新程序
func RunUpdate(deviceID string, targetExe string) error {
deviceID := device.GetDeviceID()
fmt.Printf("[updater] 当前设备id: %s\n", deviceID)
versionInt, err := strconv.Atoi(*version)
if err != nil {
log.Println("[updater] invalid version:", *version)
versionInt = 0
}
if err := RunUpdate(deviceID, versionInt); err != nil {
log.Fatalf("[updater] 更新失败: %v", err)
}
fmt.Println("[updater] 更新程序结束")
}
func RunUpdate(deviceID string, version int) error {
// 1. 检查更新
info, err := api.CheckUpdate(deviceID)
if err != nil {
fmt.Println("[updater] 请求错误,请检查网络")
return err
}
fmt.Println("[updater] 新版本:", info.Version)
fmt.Println("[updater] 新内容:", info.Notes)
fmt.Println("[updater] 下载地址:", info.DownloadURL)
// 获取主程序路径
selfPath, err := os.Executable()
if err != nil {
return err
}
// 2. 比对本地版本
if info.Version <= utils.APP_VERSION {
fmt.Println("[updater] already latest version:", utils.APP_VERSION)
return nil
}
fmt.Println("[updater] updating to version:", info.Version, "notes:", info.Notes)
selfDir := filepath.Dir(selfPath)
targetExe := filepath.Join(selfDir, "main.exe") // Windows 固定名,可根据实际改
// 3. 下载新版本到临时目录
tmpFile := filepath.Join(os.TempDir(), "new_program_tmp")
out, err := os.Create(tmpFile)
// 2. 对比版本号,没有新版本则直接启动原程序
if info.Version <= version {
fmt.Println("[updater] 暂未发现新版本,启动原程序")
cmd := exec.Command(targetExe)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if runtime.GOOS == "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
}
if err := cmd.Start(); err != nil {
return err
}
os.Exit(0)
}
// 3. 有新版本则先备份到 ./tmp/old_app/
backupDir := filepath.Join(selfDir, "tmp", "old_app")
_ = os.MkdirAll(backupDir, 0755)
backupFile := filepath.Join(backupDir, "main_"+strconv.Itoa(version)+".bak")
if err := os.Rename(targetExe, backupFile); err != nil {
fmt.Println("[updater] 备份失败,但继续更新:", err)
}
// 4. 下载新版本到 ./tmp
tmpDir := filepath.Join(selfDir, "tmp")
_ = os.MkdirAll(tmpDir, 0755)
u, err := url.Parse(info.DownloadURL)
if err != nil {
return fmt.Errorf("create temp file failed: %w", err)
return err
}
defer out.Close()
base := filepath.Base(u.Path)
ext := filepath.Ext(base)
resp2, err := http.Get(info.DownloadURL)
tmpFile, err := os.CreateTemp(tmpDir, "main_*"+ext)
if err != nil {
return fmt.Errorf("download failed: %w", err)
return err
}
defer resp2.Body.Close()
defer tmpFile.Close()
h := sha256.New()
mw := io.MultiWriter(out, h)
if _, err := io.Copy(mw, resp2.Body); err != nil {
return fmt.Errorf("write temp file failed: %w", err)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(info.DownloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
return err
}
// 4. 替换 targetExe
backup := targetExe + ".bak"
_ = os.Remove(backup)
_ = os.Rename(targetExe, backup) // 备份旧版本
if err := os.Rename(tmpFile, targetExe); err != nil {
return fmt.Errorf("replace main program failed: %w", err)
// 5. 重命名新文件到 ./main.exe
tmpFile.Close() // 关闭临时文件才能重命名
maxRetry := 20
for i := 0; i < maxRetry; i++ {
err := os.Rename(tmpFile.Name(), targetExe)
if err == nil {
break
}
fmt.Println("[updater] 文件被占用,等待 500ms 再尝试...")
time.Sleep(500 * time.Millisecond)
if i == maxRetry-1 {
return fmt.Errorf("替换失败: %w", err)
}
}
fmt.Println("[updater] replaced main program")
// 5. 启动主程序
// 6. 启动主程序,同时完全退出自己
cmd := exec.Command(targetExe)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if runtime.GOOS == "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start new program failed: %w", err)
return err
}
fmt.Println("[updater] new program started successfully")
fmt.Printf("[updater] 更新完成,新程序已启动 (pid=%d),退出更新程序\n", cmd.Process.Pid)
os.Exit(0)
return nil
}