release: v0.2.0
Some checks failed
Release / Build & Release (push) Has been cancelled
Some checks failed
Release / Build & Release (push) Has been cancelled
Comprehensive release containing structural, UX, and behavioral upgrades since v0.1.0: 1. Namespace Transition: Renamed core executable and project namespaces from 'smart-monitor' to 'smart-shutdown'. 2. Objective Vocabulary Refactoring: Normalized output strings and logging descriptors system-wide to a strict, professional tone. 3. Advanced Status Query: 'status' subcommand now retrieves the parsed configuration, log locations/sizes, and tails the last 10 lines of the local log file. 4. Runtime Configuration Setter: Introduced 'config set' subcommand to modify the configuration file with strict type validations. 5. Auto-System Deployment: Remapped 'install' to clone the executable into system domains and register global PATH variables. 6. Cleaner Removal: 'uninstall' purges binary clones and clears environmental variables, leaving zero traces. 7. Documentation Rewrite: Generated an objective README file featuring copy-paste ready Markdown blocks.
This commit is contained in:
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -31,16 +31,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-monitor_windows_amd64.exe ./cmd/smart-monitor
|
GOOS=windows GOARCH=amd64 go build -ldflags="-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-monitor_windows_arm64.exe ./cmd/smart-monitor
|
GOOS=windows GOARCH=arm64 go build -ldflags="-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-monitor_linux_amd64 ./cmd/smart-monitor
|
GOOS=linux GOARCH=amd64 go build -ldflags="-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-monitor_linux_arm64 ./cmd/smart-monitor
|
GOOS=linux GOARCH=arm64 go build -ldflags="-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
|
||||||
|
|||||||
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-monitor
|
smart-shutdown
|
||||||
smart-monitor.exe
|
smart-shutdown.exe
|
||||||
|
|
||||||
# Output of the go coverage tool
|
# Output of the go coverage tool
|
||||||
*.out
|
*.out
|
||||||
|
|||||||
91
README.md
Normal file
91
README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Smart Network Shutdown Monitor
|
||||||
|
|
||||||
|
持续监测目标网络连通性并在网络断开超时后自动关闭系统的跨平台常驻服务程序。
|
||||||
|
|
||||||
|
## 安装指南 (Installation)
|
||||||
|
|
||||||
|
本程序支持安装为操作系统的后台服务,并自动注册全局环境变量以便统一管理。
|
||||||
|
|
||||||
|
### Windows 系统
|
||||||
|
|
||||||
|
推荐以拥有管理员权限的系统服务形式部署。
|
||||||
|
|
||||||
|
1. **获取程序**
|
||||||
|
前往 [Releases](https://github.com/UnbalancedCat/smart-shutdown/releases) 页面,下载最新的 `smart-shutdown_windows_amd64.exe` (或其它架构版本)。
|
||||||
|
|
||||||
|
2. **终端安装**
|
||||||
|
在下载目录的空白处右键选择 **以管理员身份打开 PowerShell 或 CMD**,执行如下指令:
|
||||||
|
```powershell
|
||||||
|
.\smart-shutdown_windows_amd64.exe install
|
||||||
|
smart-shutdown start
|
||||||
|
```
|
||||||
|
> **说明:** `install` 指令会自动将程序复制到 `C:\Program Files\SmartShutdown\` 并写入系统 `PATH` 环境变量中,同时注册为 Windows 开机自启服务。此后您可在任意终端直接使用 `smart-shutdown` 命令。
|
||||||
|
|
||||||
|
### Linux (Ubuntu/Debian/CentOS 等)
|
||||||
|
|
||||||
|
如您具备 `sudo` 权限,可在终端中执行以下单行命令进行自动化下载与 `systemd` 服务部署:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo curl -sSL https://github.com/UnbalancedCat/smart-shutdown/releases/latest/download/smart-shutdown_linux_amd64 -o /usr/local/bin/smart-shutdown && \
|
||||||
|
sudo chmod +x /usr/local/bin/smart-shutdown && \
|
||||||
|
sudo smart-shutdown install && \
|
||||||
|
sudo smart-shutdown start
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI 终端管理指令 (Commands)
|
||||||
|
|
||||||
|
完成服务注册后,可通过以下指令直接管理守护进程状态流。
|
||||||
|
*(注: 启停服务及修改配置的指令需在 **管理员级别 / root 权限** 终端内执行)*
|
||||||
|
|
||||||
|
### 运行状态查询
|
||||||
|
输出当前服务存活状态、载入的配置参数、日志存放路径及最近 10 条日志:
|
||||||
|
```bash
|
||||||
|
smart-shutdown status
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改系统配置
|
||||||
|
直接修改配置参数并执行合法性校验。(注: 改写配置后必须执行 `smart-shutdown restart` 重启服务才能应用生效)
|
||||||
|
```bash
|
||||||
|
# 修改目标监控 IP 地址
|
||||||
|
smart-shutdown config set TargetIP 192.168.0.1
|
||||||
|
|
||||||
|
# 修改断网响应的容忍监控窗口
|
||||||
|
smart-shutdown config set MonitorWindowSeconds 300
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务生命周期控制
|
||||||
|
```bash
|
||||||
|
# 停止当前运行的后台网络探测服务
|
||||||
|
smart-shutdown stop
|
||||||
|
|
||||||
|
# 启动后台服务并恢复网络探测
|
||||||
|
smart-shutdown start
|
||||||
|
|
||||||
|
# 重启后台服务
|
||||||
|
smart-shutdown restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务与环境卸载
|
||||||
|
停止并注销后台服务,清理系统目录中的程序副本与对应的系统环境变量:
|
||||||
|
```bash
|
||||||
|
smart-shutdown uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
*(如需查看完整的帮助说明,可执行 `smart-shutdown help`)*
|
||||||
|
|
||||||
|
## 默认配置参数详情
|
||||||
|
程序会在对应操作系统的后台数据存放区建立或读取 JSON 配置文件:
|
||||||
|
- Windows: `C:\ProgramData\SmartNetworkMonitor\config.json`
|
||||||
|
- Linux: `/etc/smart-network-monitor/config.json`
|
||||||
|
|
||||||
|
| 参数项 | 含义与功能说明 | 默认值 |
|
||||||
|
|:---|:---|:---|
|
||||||
|
| `TargetIP` | 需发送 ICMP 包验证联通性的目标 IPv4 地址。 | `192.168.3.3` |
|
||||||
|
| `MonitorWindowSeconds` | 网络中断被判定为异常并触发系统关机前,所能容忍的最长超时时长 (秒)。 | `180` |
|
||||||
|
| `ShutdownCountdown` | 容忍超限后,执行正式关机系统指令的警告倒计时缓冲时间 (秒)。 | `60` |
|
||||||
|
| `NormalPingInterval` | 网络连通性正常时,每次静默发包探测的间隔时间 (秒)。 | `15` |
|
||||||
|
|
||||||
|
## 本地日志存放位置
|
||||||
|
程序按日切割保存网络探测及中断记录,默认留存最近 30 天的文件:
|
||||||
|
- **Windows**: `C:\ProgramData\SmartNetworkMonitor\logs\network_monitor.log`
|
||||||
|
- **Linux**: `/var/log/smart-network-monitor/network_monitor.log`
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"smart-shutdown/pkg/config"
|
|
||||||
"smart-shutdown/pkg/daemon"
|
|
||||||
"smart-shutdown/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 rootCommand = &cobra.Command{
|
|
||||||
Use: "smart-monitor",
|
|
||||||
Short: "智能网络监控自动关机",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
// 如果没有输入子命令,尝试作为服务直接前台或者后台跑起来
|
|
||||||
// 这个分支也是系统启动服务(systemd/services.msc)自动拉起进程时的必然入口
|
|
||||||
err := svc.Run()
|
|
||||||
if err != nil {
|
|
||||||
logger.Fail("运行抛出异常: %v", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 系统服务管理一键控制逻辑,使用 kardianos/service 提供的内置 Control方法
|
|
||||||
cmds := []*cobra.Command{
|
|
||||||
{
|
|
||||||
Use: "install",
|
|
||||||
Short: "将本程序装载并注册为系统常驻服务 (例如 systemd / windows registry)",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
handleServiceControl(svc, "install")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Use: "uninstall",
|
|
||||||
Short: "从系统中彻底卸载此后台监控服务",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
handleServiceControl(svc, "uninstall")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ 获取服务状态失败: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch status {
|
|
||||||
case service.StatusRunning:
|
|
||||||
fmt.Println("✅ 服务存活 [运行中]")
|
|
||||||
case service.StatusStopped:
|
|
||||||
fmt.Println("💤 服务处于 [已停止] 状态")
|
|
||||||
default:
|
|
||||||
fmt.Println("❓ 服务尚未在本机注册或者状态未知。请确认你是否执行过 smart-monitor install")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range cmds {
|
|
||||||
rootCommand.AddCommand(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rootCommand.Execute(); err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleServiceControl(s service.Service, action string) {
|
|
||||||
err := service.Control(s, action)
|
|
||||||
if err != nil {
|
|
||||||
logger.Crit("执行动作 [%s] 遭到拒绝或失败: %v", action, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Succ("成功对系统后台服务执行 [%s] 指令!", action)
|
|
||||||
}
|
|
||||||
@@ -2,20 +2,21 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config 存储程序的各类配置常量,支持通过读取 json 文件动态修改
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
TargetIP string `json:"TargetIP"` // 检测目标 IP,默认 192.168.3.3
|
TargetIP string `json:"TargetIP"`
|
||||||
MonitorWindowSeconds int `json:"MonitorWindowSeconds"` // 监控窗口期内持续失败才触发关机
|
MonitorWindowSeconds int `json:"MonitorWindowSeconds"`
|
||||||
ShutdownCountdown int `json:"ShutdownCountdown"` // 关机倒计时时间
|
ShutdownCountdown int `json:"ShutdownCountdown"`
|
||||||
NormalPingInterval int `json:"NormalPingInterval"` // 正常状态下的 ping 间隔
|
NormalPingInterval int `json:"NormalPingInterval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig 提供一套开箱即用的默认配置
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
TargetIP: "192.168.3.3",
|
TargetIP: "192.168.3.3",
|
||||||
@@ -25,7 +26,6 @@ func DefaultConfig() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigDir 根据当前操作系统返回标准的配置存放路径
|
|
||||||
func GetConfigDir() string {
|
func GetConfigDir() string {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return `C:\ProgramData\SmartNetworkMonitor`
|
return `C:\ProgramData\SmartNetworkMonitor`
|
||||||
@@ -33,7 +33,6 @@ func GetConfigDir() string {
|
|||||||
return `/etc/smart-network-monitor`
|
return `/etc/smart-network-monitor`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogDir 根据当前操作系统返回标准的日志存放路径
|
|
||||||
func GetLogDir() string {
|
func GetLogDir() string {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return `C:\ProgramData\SmartNetworkMonitor\logs`
|
return `C:\ProgramData\SmartNetworkMonitor\logs`
|
||||||
@@ -41,7 +40,6 @@ func GetLogDir() string {
|
|||||||
return `/var/log/smart-network-monitor`
|
return `/var/log/smart-network-monitor`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig 从系统标准路径读取 config.json 并反序列化。如果不存在,则返回默认配置。
|
|
||||||
func LoadConfig() (*Config, error) {
|
func LoadConfig() (*Config, error) {
|
||||||
configDir := GetConfigDir()
|
configDir := GetConfigDir()
|
||||||
configPath := filepath.Join(configDir, "config.json")
|
configPath := filepath.Join(configDir, "config.json")
|
||||||
@@ -49,7 +47,6 @@ func LoadConfig() (*Config, error) {
|
|||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// 文件不存在时,静默采用默认配置
|
|
||||||
return DefaultConfig(), nil
|
return DefaultConfig(), nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -65,3 +62,54 @@ func LoadConfig() (*Config, error) {
|
|||||||
}
|
}
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SaveConfig(cfg *Config) error {
|
||||||
|
configDir := GetConfigDir()
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configPath := filepath.Join(configDir, "config.json")
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(configPath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateConfig(key string, value string) error {
|
||||||
|
cfg, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加载配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "TargetIP":
|
||||||
|
if net.ParseIP(value) == nil {
|
||||||
|
return fmt.Errorf("非法 IP 地址: %s", value)
|
||||||
|
}
|
||||||
|
cfg.TargetIP = value
|
||||||
|
case "MonitorWindowSeconds":
|
||||||
|
val, err := strconv.Atoi(value)
|
||||||
|
if err != nil || val <= 0 {
|
||||||
|
return fmt.Errorf("非法输入: MonitorWindowSeconds 必须为正整数")
|
||||||
|
}
|
||||||
|
cfg.MonitorWindowSeconds = val
|
||||||
|
case "ShutdownCountdown":
|
||||||
|
val, err := strconv.Atoi(value)
|
||||||
|
if err != nil || val <= 0 {
|
||||||
|
return fmt.Errorf("非法输入: ShutdownCountdown 必须为正整数")
|
||||||
|
}
|
||||||
|
cfg.ShutdownCountdown = val
|
||||||
|
case "NormalPingInterval":
|
||||||
|
val, err := strconv.Atoi(value)
|
||||||
|
if err != nil || val <= 0 {
|
||||||
|
return fmt.Errorf("非法输入: NormalPingInterval 必须为正整数")
|
||||||
|
}
|
||||||
|
cfg.NormalPingInterval = val
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("未知配置项: %s (开放编辑项: TargetIP, MonitorWindowSeconds, ShutdownCountdown, NormalPingInterval)", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SaveConfig(cfg)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ 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 {
|
||||||
@@ -16,8 +17,7 @@ type program struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) Start(s service.Service) error {
|
func (p *program) Start(s service.Service) error {
|
||||||
// Start should not block. Do the actual work async.
|
logger.Info("启动系统监控后台服务")
|
||||||
logger.Info("准备在后台启动服务监控...")
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
p.cancel = cancel
|
p.cancel = cancel
|
||||||
@@ -28,14 +28,12 @@ func (p *program) Start(s service.Service) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) run(ctx context.Context) {
|
func (p *program) run(ctx context.Context) {
|
||||||
// 挂载执行核心循环逻辑
|
|
||||||
monitor.Run(ctx, p.cfg)
|
monitor.Run(ctx, p.cfg)
|
||||||
close(p.exit)
|
close(p.exit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *program) Stop(s service.Service) error {
|
func (p *program) Stop(s service.Service) error {
|
||||||
// Stop should not block. Return within a few seconds.
|
logger.Info("停止系统监控后台服务")
|
||||||
logger.Info("正在平滑停止后台服务监控...")
|
|
||||||
if p.cancel != nil {
|
if p.cancel != nil {
|
||||||
p.cancel()
|
p.cancel()
|
||||||
}
|
}
|
||||||
@@ -43,12 +41,11 @@ func (p *program) Stop(s service.Service) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetService 构建 service 实例供 CLI 控制(安装,启动,停止,卸载)和前台直接 Run。
|
|
||||||
func GetService(cfg *config.Config) (service.Service, error) {
|
func GetService(cfg *config.Config) (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 daemon that pings target IP periodically and shuts down the computer if disconnected for too long.",
|
Description: "A reliable daemon that periodically monitors network states and triggers node suspension logically.",
|
||||||
}
|
}
|
||||||
|
|
||||||
prg := &program{
|
prg := &program{
|
||||||
|
|||||||
@@ -1,70 +1,89 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
"smart-shutdown/pkg/config"
|
"smart-shutdown/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var fileLogger *lumberjack.Logger
|
||||||
infoLogger *log.Logger
|
|
||||||
succLogger *log.Logger
|
|
||||||
warnLogger *log.Logger
|
|
||||||
failLogger *log.Logger
|
|
||||||
critLogger *log.Logger
|
|
||||||
)
|
|
||||||
|
|
||||||
// InitLogger 初始化全局日志系统,将日志同时输出到终端色彩日志和滚动文件
|
|
||||||
func InitLogger() error {
|
func InitLogger() error {
|
||||||
logDir := config.GetLogDir()
|
logDir := config.GetLogDir()
|
||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
// 降级为当前目录,适用于未具有系统写权限的 Local 执行
|
|
||||||
logDir = "logs"
|
logDir = "logs"
|
||||||
os.MkdirAll(logDir, 0755)
|
os.MkdirAll(logDir, 0755)
|
||||||
}
|
}
|
||||||
|
|
||||||
logFile := filepath.Join(logDir, "network_monitor.log")
|
logFile := filepath.Join(logDir, "network_monitor.log")
|
||||||
|
|
||||||
// 使用 lumberjack 实现每天轮转和最大保留 30 天功能
|
fileLogger = &lumberjack.Logger{
|
||||||
ljLogger := &lumberjack.Logger{
|
|
||||||
Filename: logFile,
|
Filename: logFile,
|
||||||
MaxSize: 10, // 每个切片最大兆字节 (MB)
|
MaxSize: 10,
|
||||||
MaxBackups: 30, // 保留最近 30 个切片
|
MaxBackups: 30,
|
||||||
MaxAge: 30, // 保留最近 30 天的文件
|
MaxAge: 30,
|
||||||
Compress: false, // 是否压缩
|
Compress: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 各级别终端色彩
|
|
||||||
cSucc := color.New(color.FgGreen).SprintFunc()
|
|
||||||
cFail := color.New(color.FgRed).SprintFunc()
|
|
||||||
cWarn := color.New(color.FgYellow).SprintFunc()
|
|
||||||
cCrit := color.New(color.FgHiRed, color.Bold).SprintFunc()
|
|
||||||
|
|
||||||
// 构建复合输出写入器:终端标准输出 + 日志文件
|
|
||||||
outGeneral := io.MultiWriter(os.Stdout, ljLogger)
|
|
||||||
outError := io.MultiWriter(os.Stderr, ljLogger)
|
|
||||||
|
|
||||||
// 时间戳格式前缀
|
|
||||||
flags := log.Ldate | log.Ltime
|
|
||||||
|
|
||||||
// 实例化各个级别 Logger
|
|
||||||
infoLogger = log.New(outGeneral, "[INFO] ", flags)
|
|
||||||
succLogger = log.New(outGeneral, cSucc("[SUCCESS] "), flags)
|
|
||||||
warnLogger = log.New(outGeneral, cWarn("[WARN] "), flags)
|
|
||||||
failLogger = log.New(outError, cFail("[FAIL] "), flags)
|
|
||||||
critLogger = log.New(outError, cCrit("[CRITICAL] "), flags)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封装导出供业务调用的函数
|
func writeLog(level, plainPrefix, format string, v ...interface{}) {
|
||||||
func Info(format string, v ...interface{}) { infoLogger.Printf(format, v...) }
|
msg := fmt.Sprintf(format, v...)
|
||||||
func Succ(format string, v ...interface{}) { succLogger.Printf(format, v...) }
|
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||||
func Warn(format string, v ...interface{}) { warnLogger.Printf(format, v...) }
|
|
||||||
func Fail(format string, v ...interface{}) { failLogger.Printf(format, v...) }
|
if fileLogger != nil {
|
||||||
func Crit(format string, v ...interface{}) { critLogger.Printf(format, v...) }
|
plainLine := fmt.Sprintf("%s %s %s\n", plainPrefix, timestamp, msg)
|
||||||
|
fileLogger.Write([]byte(plainLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixColorFunc := getPrefixColor(level)
|
||||||
|
coloredPrefix := prefixColorFunc(plainPrefix)
|
||||||
|
coloredLine := fmt.Sprintf("%s %s %s\n", coloredPrefix, timestamp, msg)
|
||||||
|
|
||||||
|
if level == "FAIL" || level == "CRITICAL" {
|
||||||
|
fmt.Fprint(os.Stderr, coloredLine)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(os.Stdout, coloredLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrefixColor(level string) func(a ...interface{}) string {
|
||||||
|
switch level {
|
||||||
|
case "SUCCESS":
|
||||||
|
return color.New(color.FgGreen).SprintFunc()
|
||||||
|
case "WARN":
|
||||||
|
return color.New(color.FgYellow).SprintFunc()
|
||||||
|
case "FAIL":
|
||||||
|
return color.New(color.FgRed).SprintFunc()
|
||||||
|
case "CRITICAL":
|
||||||
|
return color.New(color.FgHiRed).SprintFunc()
|
||||||
|
default:
|
||||||
|
return color.New(color.Reset).SprintFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(format string, v ...interface{}) {
|
||||||
|
writeLog("INFO", "[INFO]", format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Succ(format string, v ...interface{}) {
|
||||||
|
writeLog("SUCCESS", "[SUCCESS]", format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(format string, v ...interface{}) {
|
||||||
|
writeLog("WARN", "[WARN]", format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fail(format string, v ...interface{}) {
|
||||||
|
writeLog("FAIL", "[FAIL]", format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Crit(format string, v ...interface{}) {
|
||||||
|
writeLog("CRITICAL", "[CRITICAL]", format, v...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,113 +10,96 @@ import (
|
|||||||
"smart-shutdown/pkg/system"
|
"smart-shutdown/pkg/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run 开启核心状态机循环监控,接收 ctx 以便主程序或者服务管理器控制安全退出
|
|
||||||
func Run(ctx context.Context, cfg *config.Config) {
|
func Run(ctx context.Context, cfg *config.Config) {
|
||||||
logger.Info("========== 智能关机监控已启动 ==========")
|
logger.Info("启动后台网络监控")
|
||||||
logger.Info("监控目标: %s", cfg.TargetIP)
|
logger.Info("监控目标 IP: %s", cfg.TargetIP)
|
||||||
logger.Info("监控窗口: %d秒", cfg.MonitorWindowSeconds)
|
logger.Info("故障判定窗口: %d秒", cfg.MonitorWindowSeconds)
|
||||||
logger.Info("关机倒计时: %d秒", cfg.ShutdownCountdown)
|
logger.Info("关机倒计时阈值: %d秒", cfg.ShutdownCountdown)
|
||||||
|
|
||||||
var failureStartTime time.Time
|
var failureStartTime time.Time
|
||||||
var inFailureWindow bool = false
|
var inFailureWindow bool = false
|
||||||
|
|
||||||
normalStatusCounter := 0
|
normalStatusCounter := 0
|
||||||
// 每 24 次正常检测输出一次日志(约 6 分钟)
|
|
||||||
const normalStatusLogInterval = 24
|
const normalStatusLogInterval = 24
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logger.Info("========== 收到停止信号,退出监控 ==========")
|
logger.Info("接收到系统停止指令, 退出核心调度监控")
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
isOnline := pinger.Ping(cfg.TargetIP, 3) // 3 秒超时探测一次
|
isOnline := pinger.Ping(cfg.TargetIP, 3)
|
||||||
|
|
||||||
if isOnline {
|
if isOnline {
|
||||||
if inFailureWindow {
|
if inFailureWindow {
|
||||||
logger.Succ("网络连接已恢复!重置断网计时器。")
|
logger.Succ("网络连通性恢复, 监控窗口重置")
|
||||||
inFailureWindow = false
|
inFailureWindow = false
|
||||||
normalStatusCounter = 0
|
normalStatusCounter = 0
|
||||||
} else {
|
} else {
|
||||||
normalStatusCounter++
|
normalStatusCounter++
|
||||||
if normalStatusCounter >= normalStatusLogInterval {
|
if normalStatusCounter >= normalStatusLogInterval {
|
||||||
logger.Info("网络连接持续正常(已检测 %d 次)", normalStatusCounter)
|
logger.Info("网络探测状态正常 (定期状态更新)")
|
||||||
normalStatusCounter = 0
|
normalStatusCounter = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 正常状态下休眠等待下一次探测
|
|
||||||
time.Sleep(time.Duration(cfg.NormalPingInterval) * time.Second)
|
time.Sleep(time.Duration(cfg.NormalPingInterval) * time.Second)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
logger.Fail("Ping 测试失败 - 目标: %s", cfg.TargetIP)
|
logger.Warn("目标节点未响应探针包: %s", cfg.TargetIP)
|
||||||
|
|
||||||
if !inFailureWindow {
|
if !inFailureWindow {
|
||||||
// 首次断网
|
|
||||||
failureStartTime = time.Now()
|
failureStartTime = time.Now()
|
||||||
inFailureWindow = true
|
inFailureWindow = true
|
||||||
logger.Warn("网络首次中断,开始进入监控窗口计时。")
|
logger.Warn("触发网络状态失效, 开始累计中断时间监控")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算当前断网累计时间
|
|
||||||
failureDuration := time.Since(failureStartTime).Seconds()
|
failureDuration := time.Since(failureStartTime).Seconds()
|
||||||
|
|
||||||
if int(failureDuration) >= cfg.MonitorWindowSeconds {
|
if int(failureDuration) >= cfg.MonitorWindowSeconds {
|
||||||
logger.Crit("网络持续中断已超过 %d 秒,触发关机流程!", cfg.MonitorWindowSeconds)
|
logger.Crit("持续中断时长超越设定的阈值上限 (%d秒), 进入关机响应流程", cfg.MonitorWindowSeconds)
|
||||||
|
|
||||||
// 进入倒计时阶段,倒计时期间高频检测(阻塞调用)
|
|
||||||
shutdownCancelled := startCountdown(ctx, cfg.TargetIP, cfg.ShutdownCountdown)
|
shutdownCancelled := startCountdown(ctx, cfg.TargetIP, cfg.ShutdownCountdown)
|
||||||
|
|
||||||
if !shutdownCancelled {
|
if !shutdownCancelled {
|
||||||
logger.Crit("倒计时结束,执行最终关机!")
|
logger.Crit("倒计时时限完毕, 开始下发系统级级终态关机指令")
|
||||||
system.ExecuteShutdown()
|
system.ExecuteShutdown()
|
||||||
// 关机指令发出,理论上系统即将终止进程,为了优雅,此处也退出
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// 倒计时中途网络恢复被取消了,重置状态重新进入监控环节
|
|
||||||
inFailureWindow = false
|
inFailureWindow = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 还在容忍窗口期内,仅仅打印日志并休眠
|
logger.Info("网络持续断失: %.0f/%d 秒", failureDuration, cfg.MonitorWindowSeconds)
|
||||||
logger.Info("网络已持续中断 %.0f / %d 秒,继续监控...", failureDuration, cfg.MonitorWindowSeconds)
|
|
||||||
time.Sleep(time.Duration(cfg.NormalPingInterval) * time.Second)
|
time.Sleep(time.Duration(cfg.NormalPingInterval) * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// startCountdown 执行关机前倒计时的轮询,如果中途恢复网络返回 true (代表取消关机)。
|
|
||||||
// 返回 false 代表断网到底,坚决关机。
|
|
||||||
func startCountdown(ctx context.Context, targetIP string, countdownSec int) bool {
|
func startCountdown(ctx context.Context, targetIP string, countdownSec int) bool {
|
||||||
logger.Warn("============================================")
|
logger.Warn("执行系统关机倒计时流程 (预设: %d秒)...", countdownSec)
|
||||||
logger.Warn("准备关机!倒计时 %d 秒期间将快速探测网络...", countdownSec)
|
|
||||||
logger.Warn("============================================")
|
|
||||||
|
|
||||||
endTime := time.Now().Add(time.Duration(countdownSec) * time.Second)
|
endTime := time.Now().Add(time.Duration(countdownSec) * time.Second)
|
||||||
// 倒计时期间提高探测频率:每 3 秒一次
|
|
||||||
ticker := time.NewTicker(3 * time.Second)
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
remaining := int(time.Until(endTime).Seconds())
|
remaining := int(time.Until(endTime).Seconds())
|
||||||
if remaining <= 0 {
|
if remaining <= 0 {
|
||||||
return false // 时间到,确认无法恢复
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Warn("距离关机还有 %d 秒... 正在高频检测网络", remaining)
|
logger.Warn("距离关机指令处分剩余: %d 秒", remaining)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logger.Info("收到退出系统服务信号,中止倒计时关机!")
|
logger.Info("挂起关机倒计流程:收到系统强制干预信号")
|
||||||
return true
|
return true
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// 高频重试检测网络
|
|
||||||
if pinger.Ping(targetIP, 3) {
|
if pinger.Ping(targetIP, 3) {
|
||||||
logger.Succ("============================================")
|
logger.Succ("倒计时周期内网络指标恢复, 全局关机流程已被重置并取消")
|
||||||
logger.Succ("网络在倒计时期间奇迹般地恢复了!立刻取消关机!")
|
|
||||||
logger.Succ("============================================")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,35 +7,28 @@ import (
|
|||||||
"smart-shutdown/pkg/logger"
|
"smart-shutdown/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ping 连通性测试,向指定的 targetIP 发送 ICMP 请求。
|
|
||||||
// 如果规定时间内可达返回 true,超时或者全部丢包返回 false。
|
|
||||||
func Ping(targetIP string, timeoutSec int) bool {
|
func Ping(targetIP string, timeoutSec int) bool {
|
||||||
pinger, err := probing.NewPinger(targetIP)
|
pinger, err := probing.NewPinger(targetIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("解析目标 IP 失败 [%s]: %v", targetIP, err)
|
logger.Warn("目标 IP 解析异常 [%s]: %v", targetIP, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 大部分操作系统为了安全,普通程序发 ICMP 包会被拦截。开启 Privileged 会尝试使用 raw sockets 发行
|
|
||||||
// 本程序由于本身作为系统服务(System 或 Root 权限)运行,故直接开启特权模式以保障发包成功率。
|
|
||||||
pinger.SetPrivileged(true)
|
pinger.SetPrivileged(true)
|
||||||
|
|
||||||
// 只发送 1 个探测包
|
|
||||||
pinger.Count = 1
|
pinger.Count = 1
|
||||||
pinger.Timeout = time.Duration(timeoutSec) * time.Second
|
pinger.Timeout = time.Duration(timeoutSec) * time.Second
|
||||||
|
|
||||||
err = pinger.Run()
|
err = pinger.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fail("Ping 测试运行时遇到异常: %v", err)
|
logger.Fail("探针执行失败: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := pinger.Statistics()
|
stats := pinger.Statistics()
|
||||||
// 只要成功收到至少 1 个回包,认为网络通畅
|
|
||||||
if stats.PacketsRecv > 0 {
|
if stats.PacketsRecv > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 丢包或者无回音
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import (
|
|||||||
"smart-shutdown/pkg/logger"
|
"smart-shutdown/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecuteShutdown 跨平台执行立即无条件关机
|
|
||||||
func ExecuteShutdown() error {
|
func ExecuteShutdown() error {
|
||||||
logger.Crit("执行系统关机操作!")
|
logger.Crit("系统调用:执行关机指令")
|
||||||
|
|
||||||
// 注意:在实际正式环境使用前,如果想防误触可暂时注释掉下方的 `cmd.Run()`
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
cmd = exec.Command("shutdown", "-s", "-f", "-t", "0")
|
cmd = exec.Command("shutdown", "-s", "-f", "-t", "0")
|
||||||
|
|||||||
Reference in New Issue
Block a user