Compare commits
3 Commits
v0.2.0
...
35bf24f0f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 35bf24f0f3 | |||
| b1c7b9f8f7 | |||
| 4e2aba5560 |
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -1,10 +1,9 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # 当推送标签形如 v1.0.0 时触发自动构建
|
- 'v*' # 褰撴帹閫佹爣绛惧舰濡?v1.0.0 鏃惰Е鍙戣嚜鍔ㄦ瀯寤?
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
@@ -31,16 +30,16 @@ jobs:
|
|||||||
mkdir -p build_output
|
mkdir -p build_output
|
||||||
|
|
||||||
echo "Building Windows amd64..."
|
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..."
|
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..."
|
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..."
|
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
|
- name: Publish GitHub Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
@@ -51,3 +50,4 @@ jobs:
|
|||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,8 +11,8 @@
|
|||||||
# Output directory for cross-platform builds
|
# Output directory for cross-platform builds
|
||||||
/bin/
|
/bin/
|
||||||
/build/
|
/build/
|
||||||
smart-shutdown
|
/smart-shutdown
|
||||||
smart-shutdown.exe
|
/smart-shutdown.exe
|
||||||
|
|
||||||
# Output of the go coverage tool
|
# Output of the go coverage tool
|
||||||
*.out
|
*.out
|
||||||
|
|||||||
349
cmd/smart-shutdown/main.go
Normal file
349
cmd/smart-shutdown/main.go
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
var verbose bool
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := logger.InitLogger(); err != nil {
|
||||||
|
fmt.Printf("无法初始化日志系统: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Crit("读取配置文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := daemon.GetService(cfg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Crit("构建后台服务实例失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showVersion bool
|
||||||
|
|
||||||
|
var rootCommand = &cobra.Command{
|
||||||
|
Use: "smart-shutdown",
|
||||||
|
Short: "智能网络状态检测与自动关机后台服务",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
if verbose {
|
||||||
|
logger.EnableDebug()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCommand.CompletionOptions.DisableDefaultCmd = true
|
||||||
|
rootCommand.Flags().BoolVarP(&showVersion, "version", "V", false, "输出当前二进制内核版本并联网拉取发布树状态")
|
||||||
|
rootCommand.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "打印底层部署及环境追溯 Debug 信息")
|
||||||
|
|
||||||
|
cmds := []*cobra.Command{
|
||||||
|
{
|
||||||
|
Use: "install",
|
||||||
|
Short: "将执行文件拷贝至系统目录并全局注入环境变量体系",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
targetDir, targetExe := getTargetSystemPath()
|
||||||
|
currentExe, _ := os.Executable()
|
||||||
|
|
||||||
|
if !strings.EqualFold(filepath.Clean(currentExe), filepath.Clean(targetExe)) {
|
||||||
|
logger.Debug("检测到跨域部署: 待本体克隆存放底层根目录: %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.Debug("成功挂载底层环境变量: Windows Machine Path 追加了 %s 指引。", targetDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetSvc, err := daemon.GetService(cfg, targetExe)
|
||||||
|
if err != nil {
|
||||||
|
logger.Crit("获取安装节点子域结构体构建回执失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleServiceControl(targetSvc, "install")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Use: "uninstall",
|
||||||
|
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.Debug("查明环境曾记录过全局部署逻辑,现已启动强力移除可执行源地址流: %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()
|
||||||
|
logger.Debug("环境变量系统清理:Windows Global Path 中的指向挂载项业已剥除卸载完成。")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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(注: 您的当前终端并不作为真正的宿存执行体。\n如目前您另行开启了 CMD 窗格在死循环执行此包前台,请人工叉掉那个窗口使其重新载入新版本!)")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Use: "start",
|
||||||
|
Short: "唤起执行守护后台",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
handleServiceControl(svc, "start")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "停机系统守护状态流",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
handleServiceControl(svc, "stop")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Use: "restart",
|
||||||
|
Short: "阻断并复用重新起跳服务控制主程",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
handleServiceControl(svc, "restart")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Use: "status",
|
||||||
|
Short: "展示全向服务参数结构、后端存活性报告与探针汇集",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
status, err := svc.Status()
|
||||||
|
|
||||||
|
fmt.Println("\n========== 运行状态 ==========")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("特权访问受抵 (查验核算需要系统特权根源准允): %v\n", err)
|
||||||
|
} else {
|
||||||
|
switch status {
|
||||||
|
case service.StatusRunning:
|
||||||
|
fmt.Println("全向系统基石服务: [运转值守中]")
|
||||||
|
case service.StatusStopped:
|
||||||
|
fmt.Println("全向系统基石服务: [已被静默挂起]")
|
||||||
|
default:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
printLastLogLines(10)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "提供运行参数结构的修载校验准入系统",
|
||||||
|
}
|
||||||
|
|
||||||
|
var configSetCmd = &cobra.Command{
|
||||||
|
Use: "set [键] [值]",
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("系统确认承接了新的设参: [%s] 指标结构被赋予了 [%s] 新制规格 \n(附留: 更易此配置文件必须人为发起 'smart-shutdown restart' 才能覆盖常驻内存的解析图谱。)\n", key, val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configCmd.AddCommand(configSetCmd)
|
||||||
|
rootCommand.AddCommand(configCmd)
|
||||||
|
|
||||||
|
for _, c := range cmds {
|
||||||
|
rootCommand.AddCommand(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCommand.InitDefaultHelpCmd()
|
||||||
|
for _, cmd := range rootCommand.Commands() {
|
||||||
|
if cmd.Name() == "help" {
|
||||||
|
cmd.Short = "按需展示其它操作指令及其详情"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rootCommand.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServiceControl(s service.Service, action string) {
|
||||||
|
err := service.Control(s, action)
|
||||||
|
if err != nil {
|
||||||
|
errStr := err.Error()
|
||||||
|
if strings.Contains(errStr, "Access is denied") || strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "拒绝访问") {
|
||||||
|
logger.Fail("管控流程 [%s] 触发越级防卫!无核准身份特设记录。请开具含有全 Root 及高配权限窗格承接口径。", action)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Crit("后台执行流程组块阻断报错 [%s] : %v", action, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
validLines := make([]string, 0, len(lines))
|
||||||
|
for _, l := range lines {
|
||||||
|
if strings.TrimSpace(l) != "" {
|
||||||
|
validLines = append(validLines, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start := len(validLines) - n
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
1
go.mod
1
go.mod
@@ -7,6 +7,7 @@ require (
|
|||||||
github.com/kardianos/service v1.2.4
|
github.com/kardianos/service v1.2.4
|
||||||
github.com/prometheus-community/pro-bing v0.8.0
|
github.com/prometheus-community/pro-bing v0.8.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
|
golang.org/x/mod v0.34.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
2
go.sum
2
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 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package daemon
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
"smart-shutdown/pkg/config"
|
"smart-shutdown/pkg/config"
|
||||||
"smart-shutdown/pkg/logger"
|
"smart-shutdown/pkg/logger"
|
||||||
"smart-shutdown/pkg/monitor"
|
"smart-shutdown/pkg/monitor"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type program struct {
|
type program struct {
|
||||||
@@ -41,13 +40,17 @@ func (p *program) Stop(s service.Service) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetService(cfg *config.Config) (service.Service, error) {
|
func GetService(cfg *config.Config, execPath ...string) (service.Service, error) {
|
||||||
svcConfig := &service.Config{
|
svcConfig := &service.Config{
|
||||||
Name: "SmartNetworkMonitor",
|
Name: "SmartNetworkMonitor",
|
||||||
DisplayName: "Smart Network Shutdown Monitor",
|
DisplayName: "Smart Network Shutdown Monitor",
|
||||||
Description: "A reliable daemon that periodically monitors network states and triggers node suspension logically.",
|
Description: "A reliable daemon that periodically monitors network states and triggers node suspension logically.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(execPath) > 0 && execPath[0] != "" {
|
||||||
|
svcConfig.Executable = execPath[0]
|
||||||
|
}
|
||||||
|
|
||||||
prg := &program{
|
prg := &program{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var fileLogger *lumberjack.Logger
|
var fileLogger *lumberjack.Logger
|
||||||
|
var debugEnabled bool
|
||||||
|
|
||||||
func InitLogger() error {
|
func InitLogger() error {
|
||||||
logDir := config.GetLogDir()
|
logDir := config.GetLogDir()
|
||||||
@@ -33,6 +34,10 @@ func InitLogger() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EnableDebug() {
|
||||||
|
debugEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
func writeLog(level, plainPrefix, format string, v ...interface{}) {
|
func writeLog(level, plainPrefix, format string, v ...interface{}) {
|
||||||
msg := fmt.Sprintf(format, v...)
|
msg := fmt.Sprintf(format, v...)
|
||||||
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||||
@@ -63,11 +68,19 @@ func getPrefixColor(level string) func(a ...interface{}) string {
|
|||||||
return color.New(color.FgRed).SprintFunc()
|
return color.New(color.FgRed).SprintFunc()
|
||||||
case "CRITICAL":
|
case "CRITICAL":
|
||||||
return color.New(color.FgHiRed).SprintFunc()
|
return color.New(color.FgHiRed).SprintFunc()
|
||||||
|
case "DEBUG":
|
||||||
|
return color.New(color.FgCyan).SprintFunc()
|
||||||
default:
|
default:
|
||||||
return color.New(color.Reset).SprintFunc()
|
return color.New(color.Reset).SprintFunc()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Debug(format string, v ...interface{}) {
|
||||||
|
if debugEnabled {
|
||||||
|
writeLog("DEBUG", "[DEBUG]", format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Info(format string, v ...interface{}) {
|
func Info(format string, v ...interface{}) {
|
||||||
writeLog("INFO", "[INFO]", format, v...)
|
writeLog("INFO", "[INFO]", format, v...)
|
||||||
}
|
}
|
||||||
|
|||||||
146
pkg/updater/updater.go
Normal file
146
pkg/updater/updater.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user