feat: implement smart self-updater with robust windows file-lock bypasses and semantic version parsing

This commit is contained in:
2026-03-24 17:09:28 +08:00
parent 4e2aba5560
commit b1c7b9f8f7
5 changed files with 322 additions and 43 deletions

146
pkg/updater/updater.go Normal file
View File

@@ -0,0 +1,146 @@
package updater
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"golang.org/x/mod/semver"
)
type githubRelease struct {
TagName string `json:"tag_name"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
}
func CheckForUpdate(currentVersion string) (hasNew bool, latestVersion string, downloadURL string) {
if currentVersion == "dev" || currentVersion == "" {
return false, "", ""
}
client := http.Client{Timeout: 8 * time.Second}
resp, err := client.Get("https://api.github.com/repos/UnbalancedCat/smart-shutdown/releases/latest")
if err != nil || resp.StatusCode != 200 {
return false, "", ""
}
defer resp.Body.Close()
var release githubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return false, "", ""
}
latestVersion = release.TagName
cv := currentVersion
if !semver.IsValid(cv) && !strings.HasPrefix(cv, "v") {
cv = "v" + cv
}
lv := latestVersion
if !semver.IsValid(lv) && !strings.HasPrefix(lv, "v") {
lv = "v" + lv
}
if semver.Compare(cv, lv) < 0 {
expectedAsset := fmt.Sprintf("smart-shutdown_%s_%s", runtime.GOOS, runtime.GOARCH)
if runtime.GOOS == "windows" {
expectedAsset += ".exe"
}
for _, asset := range release.Assets {
if asset.Name == expectedAsset {
return true, latestVersion, asset.BrowserDownloadURL
}
}
}
return false, "", ""
}
func CheckAndPrintUpdate(currentVersion string) {
fmt.Printf("当前内核版本: %s\n", currentVersion)
fmt.Println("正在检测云端发布节点是否有可用新版本...")
hasNew, latest, _ := CheckForUpdate(currentVersion)
if hasNew {
fmt.Printf("\n[发现更新] 获取到最新稳定版本: %s\n", latest)
fmt.Println("请执行 'smart-shutdown update' 以全自动获取并覆盖部署该更新。")
} else {
fmt.Println("当前已是最新运行版本,暂无可用更新。")
}
}
func DownloadAndReplace(downloadURL string) error {
currentExe, err := os.Executable()
if err != nil {
return fmt.Errorf("无法溯源自身执行路径: %v", err)
}
tempExe := filepath.Join(os.TempDir(), "smart-shutdown-update.tmp")
out, err := os.Create(tempExe)
if err != nil {
return fmt.Errorf("系统缓存区句柄开辟失败: %v", err)
}
resp, err := http.Get(downloadURL)
if err != nil || resp.StatusCode != 200 {
out.Close()
os.Remove(tempExe)
return fmt.Errorf("网络传输流建立失败,节点远端可能受限")
}
_, err = io.Copy(out, resp.Body)
resp.Body.Close()
out.Close()
if err != nil {
os.Remove(tempExe)
return fmt.Errorf("物理覆盖字节流中断: %v", err)
}
if runtime.GOOS == "windows" {
oldExe := currentExe + ".old"
os.Remove(oldExe)
if err := os.Rename(currentExe, oldExe); err != nil {
return fmt.Errorf("操作系统拒绝进程脱壳 (文件锁互斥): %v", err)
}
}
if err := copyFile(tempExe, currentExe); err != nil {
if runtime.GOOS == "windows" {
os.Rename(currentExe+".old", currentExe)
}
return fmt.Errorf("原子级覆盖执行核心挫败: %v", err)
}
os.Remove(tempExe)
if runtime.GOOS != "windows" {
os.Chmod(currentExe, 0755)
}
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()
_, err = io.Copy(out, in)
return err
}