物联网软件
This commit is contained in:
+150
-93
@@ -1,132 +1,189 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sentinel/pkg/config"
|
||||
"sentinel/pkg/device"
|
||||
"sentinel/pkg/log"
|
||||
"sentinel/pkg/net"
|
||||
"strconv"
|
||||
"sentinel/pkg/platform"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 定义命令行参数
|
||||
version := flag.String("version", "", "current version of main program")
|
||||
log.Println("[daemon] 设备ID:", device.GetDeviceID())
|
||||
|
||||
flag.Parse()
|
||||
log.Init(config.LOG_FILE_DIC)
|
||||
|
||||
if *version == "" {
|
||||
// updater 视角:-1 表示“未知版本”,一定触发更新检测
|
||||
*version = "0"
|
||||
log.Println("[updater] --version not provided, fallback to -1")
|
||||
fmt.Println("[updater] 主程序版本号:", *version)
|
||||
}
|
||||
starter := newMainProgramStarter()
|
||||
// 启动主程序
|
||||
initMainProcess(starter)
|
||||
ticker := time.NewTicker(config.DAEMON_UPDATE_CHECK_INTERVAL_SECONDS * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
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] 更新程序结束")
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
//log.Println("[daemon] 定时检查主程序运行状态")
|
||||
handleProcessCheck(starter)
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
if cfg.NeedUpdate {
|
||||
log.Println("[daemon] 配置文件标记需要更新,触发更新")
|
||||
cfg.NeedUpdate = false
|
||||
if err := config.WriteLocalConfig(cfg); err != nil {
|
||||
log.Println("[daemon] 写回配置文件失败:", err)
|
||||
}
|
||||
handleUpdate(starter)
|
||||
}
|
||||
}
|
||||
}()
|
||||
select {}
|
||||
}
|
||||
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)
|
||||
|
||||
func initMainProcess(starter platform.MainProgramStarter) {
|
||||
if err := startMainProgram(starter); err != nil {
|
||||
log.Println("[daemon] 启动失败,回退到更新:", err)
|
||||
handleUpdate(starter)
|
||||
}
|
||||
}
|
||||
|
||||
var lastState string
|
||||
var lastPID int
|
||||
|
||||
func handleProcessCheck(starter platform.MainProgramStarter) {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
// 1. PID 是否存在
|
||||
running, err := starter.IsProcessRunning(cfg.PID)
|
||||
if cfg.PID <= 0 || err != nil || !running {
|
||||
if lastState != "NOT_RUNNING" {
|
||||
log.Println("[daemon] 主程序未运行,尝试启动")
|
||||
lastState = "NOT_RUNNING"
|
||||
lastPID = 0
|
||||
}
|
||||
restartMain(starter)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 语义健康判断
|
||||
switch cfg.ControlState {
|
||||
case "DEGRADED":
|
||||
if lastState != "DEGRADED" {
|
||||
log.Println("[daemon] 主程序存在错误,等待主程序自行处理")
|
||||
lastState = "DEGRADED"
|
||||
lastPID = cfg.PID
|
||||
}
|
||||
return
|
||||
case "CONTROL_LOST":
|
||||
if lastState != "CONTROL_LOST" {
|
||||
log.Println("[daemon] 主程序心跳超时,已失控,强制重启")
|
||||
lastState = "CONTROL_LOST"
|
||||
lastPID = cfg.PID
|
||||
}
|
||||
restartMain(starter)
|
||||
return
|
||||
default:
|
||||
// 3. 健康
|
||||
if lastState != "HEALTHY" || lastPID != cfg.PID {
|
||||
log.Println("[daemon] 主程序健康运行,PID:", cfg.PID)
|
||||
lastState = "HEALTHY"
|
||||
lastPID = cfg.PID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restartMain(starter platform.MainProgramStarter) {
|
||||
cfg := config.LoadConfig()
|
||||
if cfg.PID > 0 {
|
||||
_ = starter.KillProcess(cfg.PID)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
_ = startMainProgram(starter)
|
||||
}
|
||||
|
||||
func handleUpdate(starter platform.MainProgramStarter) {
|
||||
cfg := config.LoadConfig()
|
||||
info, err := api.CheckUpdate(device.GetDeviceID())
|
||||
if err != nil {
|
||||
log.Println("[daemon] 检查更新失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if info.Version <= cfg.VersionCode {
|
||||
log.Println("[daemon] 没有新版本,跳过")
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[daemon] 发现新版本 %d\n", info.Version)
|
||||
|
||||
// 停止主程序
|
||||
if cfg.PID > 0 {
|
||||
_ = starter.KillProcess(cfg.PID)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err := downloadAndReplace(starter, info.DownloadURL); err != nil {
|
||||
log.Println("[daemon] 更新失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := startMainProgram(starter); err != nil {
|
||||
log.Println("[daemon] 更新后启动失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[daemon] 更新完成")
|
||||
}
|
||||
|
||||
func startMainProgram(starter platform.MainProgramStarter) error {
|
||||
selfPath, _ := os.Executable()
|
||||
selfDir := filepath.Dir(selfPath)
|
||||
targetExe := filepath.Join(selfDir, starter.GetMainName())
|
||||
|
||||
if _, err := os.Stat(targetExe); os.IsNotExist(err) {
|
||||
log.Println("[daemon] 主程序文件不存在,触发更新")
|
||||
handleUpdate(starter)
|
||||
return nil // 先不启动,等更新完再启动
|
||||
}
|
||||
|
||||
err := starter.Start(targetExe)
|
||||
return err
|
||||
}
|
||||
|
||||
func downloadAndReplace(
|
||||
starter platform.MainProgramStarter,
|
||||
downloadURL string,
|
||||
) error {
|
||||
// 获取主程序路径
|
||||
selfPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取自身路径失败: %w", err)
|
||||
}
|
||||
selfDir := filepath.Dir(selfPath)
|
||||
|
||||
starter := newMainProgramStarter()
|
||||
targetExe := filepath.Join(selfDir, starter.GetMainName())
|
||||
|
||||
// 2. 对比版本号,没有新版本则直接启动原程序
|
||||
if info.Version <= version {
|
||||
fmt.Println("[updater] 暂未发现新版本,启动原程序")
|
||||
if err := starter.Start(targetExe); 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)
|
||||
outFile, err := os.Create(targetExe)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建目标文件失败: %w", err)
|
||||
}
|
||||
base := filepath.Base(u.Path)
|
||||
ext := filepath.Ext(base)
|
||||
|
||||
tmpFile, err := os.CreateTemp(tmpDir, "main_*"+ext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
defer outFile.Close()
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Get(info.DownloadURL)
|
||||
resp, err := client.Get(downloadURL)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("下载新程序失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
|
||||
return err
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("下载失败, HTTP状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
if _, err := io.Copy(outFile, resp.Body); err != nil {
|
||||
return fmt.Errorf("写入目标文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 6. 启动主程序,同时完全退出自己
|
||||
if err := starter.Start(targetExe); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("[updater] 更新完成,新程序已启动,退出更新程序")
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package main
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type mainProgramStarter struct{}
|
||||
@@ -25,3 +26,29 @@ func (s *mainProgramStarter) Start(targetExe string) error {
|
||||
// Linux 下:先保持最简单,保证能跑
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// 判断进程是否在运行
|
||||
func (s *mainProgramStarter) IsProcessRunning(pid int) (bool, error) {
|
||||
if pid <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
procPath := "/proc/" + strconv.Itoa(pid)
|
||||
_, err := os.Stat(procPath)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 其他错误(例如权限问题)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 杀掉进程
|
||||
func (l *mainProgramStarter) KillProcess(pid int) error {
|
||||
cmd := exec.Command("kill", "-9", strconv.Itoa(pid))
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ package main
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type mainProgramStarter struct{}
|
||||
@@ -27,3 +30,34 @@ func (s *mainProgramStarter) Start(targetExe string) error {
|
||||
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// 判断进程是否在运行
|
||||
func (s *mainProgramStarter) IsProcessRunning(pid int) (bool, error) {
|
||||
if pid <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 打开进程句柄
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
|
||||
if err != nil {
|
||||
// 权限不足,但进程存在
|
||||
if err == windows.ERROR_ACCESS_DENIED {
|
||||
return true, nil
|
||||
}
|
||||
// 进程不存在
|
||||
if err == windows.ERROR_INVALID_PARAMETER {
|
||||
return false, nil
|
||||
}
|
||||
// 其他错误
|
||||
return false, err
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 杀掉进程
|
||||
func (w *mainProgramStarter) KillProcess(pid int) error {
|
||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user