diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b61a25..ebd5fe0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,9 @@ -name: Release +name: Release on: push: tags: - - 'v*' # 当推送标签形如 v1.0.0 时触发自动构建 - + - 'v*' # 褰撴帹閫佹爣绛惧舰濡?v1.0.0 鏃惰Е鍙戣嚜鍔ㄦ瀯寤? permissions: contents: write @@ -31,16 +30,16 @@ jobs: mkdir -p build_output echo "Building Windows amd64..." - GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o build_output/smart-shutdown_windows_amd64.exe ./cmd/smart-shutdown + GOOS=windows GOARCH=amd64 go build -ldflags="-X 'main.AppVersion=${{ github.ref_name }}' -s -w" -o build_output/smart-shutdown_windows_amd64.exe ./cmd/smart-shutdown echo "Building Windows arm64..." - GOOS=windows GOARCH=arm64 go build -ldflags="-s -w" -o build_output/smart-shutdown_windows_arm64.exe ./cmd/smart-shutdown + GOOS=windows GOARCH=arm64 go build -ldflags="-X 'main.AppVersion=${{ github.ref_name }}' -s -w" -o build_output/smart-shutdown_windows_arm64.exe ./cmd/smart-shutdown echo "Building Linux amd64..." - GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o build_output/smart-shutdown_linux_amd64 ./cmd/smart-shutdown + GOOS=linux GOARCH=amd64 go build -ldflags="-X 'main.AppVersion=${{ github.ref_name }}' -s -w" -o build_output/smart-shutdown_linux_amd64 ./cmd/smart-shutdown echo "Building Linux arm64..." - GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o build_output/smart-shutdown_linux_arm64 ./cmd/smart-shutdown + GOOS=linux GOARCH=arm64 go build -ldflags="-X 'main.AppVersion=${{ github.ref_name }}' -s -w" -o build_output/smart-shutdown_linux_arm64 ./cmd/smart-shutdown - name: Publish GitHub Release uses: softprops/action-gh-release@v1 @@ -51,3 +50,4 @@ jobs: generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/cmd/smart-shutdown/main.go b/cmd/smart-shutdown/main.go index 373ea82..a8d7cf6 100644 --- a/cmd/smart-shutdown/main.go +++ b/cmd/smart-shutdown/main.go @@ -4,17 +4,21 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" + "runtime" "strings" - "smart-shutdown/pkg/config" - "smart-shutdown/pkg/daemon" - "smart-shutdown/pkg/logger" - "github.com/kardianos/service" "github.com/spf13/cobra" + "smart-shutdown/pkg/config" + "smart-shutdown/pkg/daemon" + "smart-shutdown/pkg/logger" + "smart-shutdown/pkg/updater" ) +var AppVersion = "dev" + func main() { if err := logger.InitLogger(); err != nil { fmt.Printf("无法初始化日志系统: %v\n", err) @@ -23,98 +27,197 @@ func main() { cfg, err := config.LoadConfig() if err != nil { - logger.Crit("读取配置失败: %v", err) + logger.Crit("读取配置文件失败: %v", err) os.Exit(1) } svc, err := daemon.GetService(cfg) if err != nil { - logger.Crit("构建服务对象失败: %v", err) + logger.Crit("构建后台服务实例失败: %v", err) os.Exit(1) } + var showVersion bool + var rootCommand = &cobra.Command{ Use: "smart-shutdown", Short: "智能网络状态检测与自动关机后台服务", Run: func(cmd *cobra.Command, args []string) { + if showVersion { + fmt.Printf("========== Smart Network Shutdown Monitor ==========\n") + fmt.Printf("执行架构体基准: %s/%s\n", runtime.GOOS, runtime.GOARCH) + updater.CheckAndPrintUpdate(AppVersion) + return + } + err := svc.Run() if err != nil { - logger.Fail("运行异常: %v", err) + logger.Fail("后台监控流崩溃: %v", err) } }, } - // 禁用生成补全帮助 rootCommand.CompletionOptions.DisableDefaultCmd = true + rootCommand.Flags().BoolVarP(&showVersion, "version", "V", false, "输出当前二进制内核版本并联网拉取发布树状态") cmds := []*cobra.Command{ { Use: "install", - Short: "安装系统常驻服务", + Short: "将执行文件拷贝至系统目录并全局注入环境变量体系", Run: func(cmd *cobra.Command, args []string) { - handleServiceControl(svc, "install") + targetDir, targetExe := getTargetSystemPath() + currentExe, _ := os.Executable() + + if !strings.EqualFold(filepath.Clean(currentExe), filepath.Clean(targetExe)) { + logger.Info("预备自动配置系统级全局环境,本体克隆下放目录: %s", targetExe) + if err := os.MkdirAll(targetDir, 0755); err != nil { + logger.Fail("创建系统目录核心区路径溃败 (请核查最高系统权限身份执行需求): %v", err) + return + } + if err := copyFile(currentExe, targetExe); err != nil { + logger.Fail("注入独立执行程序副本被拒: %v", err) + return + } + + if runtime.GOOS != "windows" { + os.Chmod(targetExe, 0755) + } else { + envSetupCmd := fmt.Sprintf(`$p=[Environment]::GetEnvironmentVariable("Path","Machine");if(-not($p -split ';' -contains "%s")){[Environment]::SetEnvironmentVariable("Path",$p+";%s","Machine")}`, targetDir, targetDir) + err := exec.Command("powershell", "-Command", envSetupCmd).Run() + if err != nil { + logger.Warn("改写操作系统的环境变量集合遭受阻拦: %v", err) + } else { + logger.Info("执行路径已完备挂载进 Windows Global PATH 域内,用户可通过全局指引调取 smart-shutdown。") + } + } + } + + targetSvc, err := daemon.GetService(cfg, targetExe) + if err != nil { + logger.Crit("获取安装节点子域结构体构建回执失败: %v", err) + return + } + handleServiceControl(targetSvc, "install") }, }, { Use: "uninstall", - Short: "卸载系统常驻服务", + Short: "废除并移除在册的后台服务、扫除环境关联残留", Run: func(cmd *cobra.Command, args []string) { service.Control(svc, "stop") handleServiceControl(svc, "uninstall") + + targetDir, targetExe := getTargetSystemPath() + currentExe, _ := os.Executable() + + if !strings.EqualFold(filepath.Clean(currentExe), filepath.Clean(targetExe)) { + if _, err := os.Stat(targetExe); err == nil { + logger.Info("查明曾执行过部署注入逻辑,现在开始彻底摘毁并清理目录: %s", targetExe) + os.Remove(targetExe) + + if runtime.GOOS == "windows" { + os.Remove(targetDir) + envClearCmd := fmt.Sprintf(`$p=[Environment]::GetEnvironmentVariable("Path","Machine");$np=($p -split ';' | Where-Object {$_ -ne "%s" -and $_ -ne ""}) -join ';';[Environment]::SetEnvironmentVariable("Path",$np,"Machine")`, targetDir) + exec.Command("powershell", "-Command", envClearCmd).Run() + } + } + } + }, + }, + { + Use: "update", + Short: "获取并免干预热部署在线的最新系统构建程序", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("当前内核架构版本: %s\n正在向全向节点请求效验最新发版记录...\n", AppVersion) + hasNew, latestVer, dlURL := updater.CheckForUpdate(AppVersion) + if !hasNew { + fmt.Println("\n[核验完毕] 您当前的程序正处于主线分支顶点,无需任何更新动作。") + return + } + + fmt.Printf("\n[匹配成功] 查收最新稳定版释出包裹: %s\n", latestVer) + fmt.Println("预备接管环境树并构建热更新映射管道...") + + status, _ := svc.Status() + isRunningService := (status == service.StatusRunning) + + if isRunningService { + fmt.Println("[生命周期管控] 已探明应用正交由系统守护树作为后台运行,正在为其下放休眠截停指派以退换抢驻的内核独占锁...") + service.Control(svc, "stop") + } + + fmt.Println("[数据流传输] 正在拉取远端预编译二进制核心封包...") + if err := updater.DownloadAndReplace(dlURL); err != nil { + fmt.Printf("[中断] 内核代码层执行热重载遭挫回滚: %v\n", err) + if isRunningService { + service.Control(svc, "start") + } + return + } + + fmt.Println("[实体部署] 核心节点原子级替换覆写完成,系统块检验通过!") + + if isRunningService { + fmt.Println("[后端苏醒] 重新挂载启动系统最高级权限守护网络接驳中心...") + service.Control(svc, "start") + fmt.Println("============ 热更新无感流闭环执行彻底成功! ============") + } else { + fmt.Println("============ 热更核心接替执行完毕!============\n(注: 您的当前终端并不作为真正的宿存执行体。如目前您另行开启了 CMD 窗格在死循环执行此包前台,请人工叉掉那个窗口使其重新载入新版本!)") + } }, }, { Use: "start", - Short: "启动后台运行服务", + Short: "唤起执行守护后台", Run: func(cmd *cobra.Command, args []string) { handleServiceControl(svc, "start") }, }, { Use: "stop", - Short: "停止后台运行服务", + Short: "停机系统守护状态流", Run: func(cmd *cobra.Command, args []string) { handleServiceControl(svc, "stop") }, }, { Use: "restart", - Short: "重启服务", + Short: "阻断并复用重新起跳服务控制主程", Run: func(cmd *cobra.Command, args []string) { handleServiceControl(svc, "restart") }, }, { Use: "status", - Short: "查询核心配置、服务运行状态及近期日志", + Short: "展示全向服务参数结构、后端存活性报告与探针汇集", Run: func(cmd *cobra.Command, args []string) { status, err := svc.Status() fmt.Println("\n========== 运行状态 ==========") if err != nil { - fmt.Printf("服务状态查询失败 (需系统管理员权限): %v\n", err) + fmt.Printf("特权访问受抵 (查验核算需要系统特权根源准允): %v\n", err) } else { switch status { case service.StatusRunning: - fmt.Println("系统服务: [运行中]") + fmt.Println("全向系统基石服务: [运转值守中]") case service.StatusStopped: - fmt.Println("系统服务: [已停止]") + fmt.Println("全向系统基石服务: [已被静默挂起]") default: - fmt.Println("系统服务: [未注册或状态未知]") + fmt.Println("全向系统基石服务: [尚未注册落位 / 孤儿状态]") } } fmt.Println("\n========== 核心配置 ==========") - fmt.Printf("探测目标 IP : %s\n", cfg.TargetIP) - fmt.Printf("容忍断连窗口 (秒) : %d\n", cfg.MonitorWindowSeconds) - fmt.Printf("预警关机倒计 (秒) : %d\n", cfg.ShutdownCountdown) - fmt.Printf("平稳探测频次 (秒) : %d\n", cfg.NormalPingInterval) + fmt.Printf("探测标的机器 IP : %s\n", cfg.TargetIP) + fmt.Printf("脱网迟滞容忍 (秒) : %d\n", cfg.MonitorWindowSeconds) + fmt.Printf("临危核准倒计 (秒) : %d\n", cfg.ShutdownCountdown) + fmt.Printf("静默发包跳频 (秒) : %d\n", cfg.NormalPingInterval) logFilePath := filepath.Join(config.GetLogDir(), "network_monitor.log") if fi, fileErr := os.Stat(logFilePath); fileErr == nil { fmt.Println("\n========== 日志系统 ==========") - fmt.Printf("日志位置: %s\n", logFilePath) - fmt.Printf("当前占用: %.2f KB\n", float64(fi.Size())/1024) + fmt.Printf("硬盘归档落点: %s\n", logFilePath) + fmt.Printf("现时容量尺寸: %.2f KB\n", float64(fi.Size())/1024) } printLastLogLines(10) @@ -124,21 +227,21 @@ func main() { var configCmd = &cobra.Command{ Use: "config", - Short: "管理运行配置文件", + Short: "提供运行参数结构的修载校验准入系统", } var configSetCmd = &cobra.Command{ Use: "set [键] [值]", - Short: "修改指定的配置参数 (例如 config set TargetIP 192.168.1.1)", + Short: "安全的对 JSON 属性执行改写封装验证 (如 config set TargetIP 192.168.3.1)", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { key := args[0] val := args[1] err := config.UpdateConfig(key, val) if err != nil { - fmt.Printf("修改配置失败: %v\n", err) + fmt.Printf("基于硬编码的防呆数据验证发回抵拦截口指令: %v\n", err) } else { - fmt.Printf("成功将配置 [%s] 更新为 [%s]。\n(提示: 请主动执行 smart-shutdown restart 使更改生效)\n", key, val) + fmt.Printf("系统确认承接了新的设参: [%s] 指标结构被赋予了 [%s] 新制规格 \n(附留: 更易此配置文件必须人为发起 'smart-shutdown restart' 才能覆盖常驻内存的解析图谱。)\n", key, val) } }, } @@ -150,11 +253,10 @@ func main() { rootCommand.AddCommand(c) } - // 初始化并汉化帮助菜单 rootCommand.InitDefaultHelpCmd() for _, cmd := range rootCommand.Commands() { if cmd.Name() == "help" { - cmd.Short = "获取任意指令的帮助文档" + cmd.Short = "按需展示其它操作指令及其详情" } } @@ -168,20 +270,20 @@ func handleServiceControl(s service.Service, action string) { if err != nil { errStr := err.Error() if strings.Contains(errStr, "Access is denied") || strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "拒绝访问") { - logger.Fail("执行动作 [%s] 失败: 权限遭拒。请以 Administrator 或 Root 权限重新执行。", action) + logger.Fail("管控流程 [%s] 触发越级防卫!无核准身份特设记录。请开具含有全 Root 及高配权限窗格承接口径。", action) return } - logger.Crit("执行动作 [%s] 失败: %v", action, err) + logger.Crit("后台执行流程组块阻断报错 [%s] : %v", action, err) return } - logger.Succ("执行动作 [%s] 成功", action) + logger.Succ("指令动作 [%s] 解析下发执行完毕,无阻断警告。", action) } func printLastLogLines(n int) { logFilePath := filepath.Join(config.GetLogDir(), "network_monitor.log") file, err := os.Open(logFilePath) if err != nil { - fmt.Printf("\n(暂无历史探测日志)\n") + fmt.Printf("\n(排障辅录: 获取探针断联记录池为空,全空栈态)\n") return } defer file.Close() @@ -204,8 +306,36 @@ func printLastLogLines(n int) { start = 0 } - fmt.Printf("\n========== 最近 %d 条抓取日志 ==========\n", n) + fmt.Printf("\n========== 最新沿线抓取探测轨迹尾部排 %d 行 ==========\n", n) for i := start; i < len(validLines); i++ { fmt.Println(validLines[i]) } } + +func getTargetSystemPath() (dir string, exe string) { + if runtime.GOOS == "windows" { + dir = filepath.Join(os.Getenv("ProgramFiles"), "SmartShutdown") + exe = filepath.Join(dir, "smart-shutdown.exe") + } else { + dir = "/usr/local/bin" + exe = filepath.Join(dir, "smart-shutdown") + } + return dir, exe +} + +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 +} diff --git a/go.mod b/go.mod index 8d776f0..f6edf91 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/kardianos/service v1.2.4 github.com/prometheus-community/pro-bing v0.8.0 github.com/spf13/cobra v1.10.2 + golang.org/x/mod v0.34.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) diff --git a/go.sum b/go.sum index c66391a..92a5e25 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= diff --git a/pkg/updater/updater.go b/pkg/updater/updater.go new file mode 100644 index 0000000..56b6ec7 --- /dev/null +++ b/pkg/updater/updater.go @@ -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 +}