147 lines
3.4 KiB
Go
147 lines
3.4 KiB
Go
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
|
|
}
|