Compare commits
7 Commits
7ed04262c3
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b1c7b9f8f7 | |||
| 4e2aba5560 | |||
| 3be3e19e49 | |||
| 3525a59976 | |||
| f288be2a63 | |||
|
|
82eeee50f1 | ||
| 4b8e3da812 |
53
.github/workflows/release.yml
vendored
Normal file
53
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*' # 褰撴帹閫佹爣绛惧舰濡?v1.0.0 鏃惰Е鍙戣嚜鍔ㄦ瀯寤?
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build & Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Download Dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Compile Cross-Platform Binaries
|
||||||
|
run: |
|
||||||
|
mkdir -p build_output
|
||||||
|
|
||||||
|
echo "Building Windows amd64..."
|
||||||
|
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="-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="-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="-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
|
||||||
|
with:
|
||||||
|
files: build_output/*
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
generate_release_notes: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output directory for cross-platform builds
|
||||||
|
/bin/
|
||||||
|
/build/
|
||||||
|
/smart-shutdown
|
||||||
|
/smart-shutdown.exe
|
||||||
|
|
||||||
|
# Output of the go coverage tool
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# IDE configs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Local logs or configuration testing directories
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
config.json
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 UnbalancedCat
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
173
README.md
173
README.md
@@ -1,122 +1,91 @@
|
|||||||
# 智能网络监控脚本 - 系统部署版
|
# Smart Network Shutdown Monitor
|
||||||
|
|
||||||
## 📁 文件结构
|
持续监测目标网络连通性并在网络断开超时后自动关闭系统的跨平台常驻服务程序。
|
||||||
|
|
||||||
```
|
## 安装指南 (Installation)
|
||||||
smart_shutdown/
|
|
||||||
├── smart_shutdown.ps1 # 原始监控脚本(用户模式)
|
|
||||||
├── deploy_system.ps1 # 系统级部署脚本
|
|
||||||
├── uninstall_system.ps1 # 系统级卸载脚本
|
|
||||||
├── TEST_REPORT.md # 测试报告
|
|
||||||
├── README.md # 本文件
|
|
||||||
└── logs/ # 本地测试日志目录
|
|
||||||
└── network_monitor_20250606.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 快速部署
|
本程序支持安装为操作系统的后台服务,并自动注册全局环境变量以便统一管理。
|
||||||
|
|
||||||
### 系统级部署(推荐)
|
### Windows 系统
|
||||||
|
|
||||||
|
推荐以拥有管理员权限的系统服务形式部署。
|
||||||
|
|
||||||
|
1. **获取程序**
|
||||||
|
前往 [Releases](https://github.com/UnbalancedCat/smart-shutdown/releases) 页面,下载最新的 `smart-shutdown_windows_amd64.exe` (或其它架构版本)。
|
||||||
|
|
||||||
|
2. **终端安装**
|
||||||
|
在下载目录的空白处右键选择 **以管理员身份打开 PowerShell 或 CMD**,执行如下指令:
|
||||||
```powershell
|
```powershell
|
||||||
# 以管理员身份运行
|
.\smart-shutdown_windows_amd64.exe install
|
||||||
.\deploy_system.ps1
|
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)
|
||||||
- 程序部署到:`C:\Program Files\SmartNetworkMonitor\`
|
|
||||||
- 配置和日志:`C:\ProgramData\SmartNetworkMonitor\`
|
|
||||||
- 开机自启动,无需用户登录
|
|
||||||
- 以 SYSTEM 权限运行
|
|
||||||
|
|
||||||
### 用户级运行
|
完成服务注册后,可通过以下指令直接管理守护进程状态流。
|
||||||
```powershell
|
*(注: 启停服务及修改配置的指令需在 **管理员级别 / root 权限** 终端内执行)*
|
||||||
# 以管理员身份运行(需要用户登录)
|
|
||||||
.\smart_shutdown.ps1
|
### 运行状态查询
|
||||||
|
输出当前服务存活状态、载入的配置参数、日志存放路径及最近 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
|
||||||
C:\Program Files\SmartNetworkMonitor\
|
|
||||||
├── smart_shutdown_system.ps1 # 系统优化版监控脚本
|
|
||||||
└── manage.ps1 # 管理工具脚本
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 数据文件位置
|
### 服务生命周期控制
|
||||||
```
|
```bash
|
||||||
C:\ProgramData\SmartNetworkMonitor\
|
# 停止当前运行的后台网络探测服务
|
||||||
├── config.json # 配置文件
|
smart-shutdown stop
|
||||||
└── logs\ # 系统日志目录
|
|
||||||
└── network_monitor_YYYYMMDD.log
|
# 启动后台服务并恢复网络探测
|
||||||
|
smart-shutdown start
|
||||||
|
|
||||||
|
# 重启后台服务
|
||||||
|
smart-shutdown restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 管理命令
|
### 服务与环境卸载
|
||||||
|
停止并注销后台服务,清理系统目录中的程序副本与对应的系统环境变量:
|
||||||
### 查看任务状态
|
```bash
|
||||||
```powershell
|
smart-shutdown uninstall
|
||||||
Get-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 启动/停止监控
|
*(如需查看完整的帮助说明,可执行 `smart-shutdown help`)*
|
||||||
```powershell
|
|
||||||
Start-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
Stop-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 查看日志
|
## 默认配置参数详情
|
||||||
```powershell
|
程序会在对应操作系统的后台数据存放区建立或读取 JSON 配置文件:
|
||||||
# 查看今天的日志
|
- Windows: `C:\ProgramData\SmartNetworkMonitor\config.json`
|
||||||
Get-Content "C:\ProgramData\SmartNetworkMonitor\logs\network_monitor_$(Get-Date -Format 'yyyyMMdd').log"
|
- Linux: `/etc/smart-network-monitor/config.json`
|
||||||
|
|
||||||
# 实时监控日志
|
| 参数项 | 含义与功能说明 | 默认值 |
|
||||||
Get-Content "C:\ProgramData\SmartNetworkMonitor\logs\network_monitor_$(Get-Date -Format 'yyyyMMdd').log" -Wait
|
|:---|:---|:---|
|
||||||
```
|
| `TargetIP` | 需发送 ICMP 包验证联通性的目标 IPv4 地址。 | `192.168.3.3` |
|
||||||
|
| `MonitorWindowSeconds` | 网络中断被判定为异常并触发系统关机前,所能容忍的最长超时时长 (秒)。 | `180` |
|
||||||
|
| `ShutdownCountdown` | 容忍超限后,执行正式关机系统指令的警告倒计时缓冲时间 (秒)。 | `60` |
|
||||||
|
| `NormalPingInterval` | 网络连通性正常时,每次静默发包探测的间隔时间 (秒)。 | `15` |
|
||||||
|
|
||||||
### 查看系统事件日志
|
## 本地日志存放位置
|
||||||
```powershell
|
程序按日切割保存网络探测及中断记录,默认留存最近 30 天的文件:
|
||||||
Get-EventLog -LogName Application -Source SmartNetworkMonitor -Newest 20
|
- **Windows**: `C:\ProgramData\SmartNetworkMonitor\logs\network_monitor.log`
|
||||||
```
|
- **Linux**: `/var/log/smart-network-monitor/network_monitor.log`
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
|
||||||
|
|
||||||
系统部署后,配置文件位于:`C:\ProgramData\SmartNetworkMonitor\config.json`
|
|
||||||
|
|
||||||
默认配置:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"TargetIP": "192.168.3.3",
|
|
||||||
"MonitorWindowSeconds": 180,
|
|
||||||
"ShutdownCountdown": 60,
|
|
||||||
"NormalPingInterval": 15
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
修改配置文件后,重启监控任务使配置生效:
|
|
||||||
```powershell
|
|
||||||
Stop-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
Start-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🗑️ 卸载
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 以管理员身份运行
|
|
||||||
.\uninstall_system.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 特性
|
|
||||||
|
|
||||||
- ✅ 开机自启动,无需用户登录
|
|
||||||
- ✅ 系统级权限运行
|
|
||||||
- ✅ 智能日志管理
|
|
||||||
- ✅ 配置文件支持
|
|
||||||
- ✅ 事件日志备份
|
|
||||||
- ✅ 自动日志清理(30天)
|
|
||||||
- ✅ 网络恢复自动取消关机
|
|
||||||
- ✅ 完整的错误处理
|
|
||||||
|
|
||||||
## 🔐 安全说明
|
|
||||||
|
|
||||||
- 脚本以 SYSTEM 权限运行,具有执行关机的完整权限
|
|
||||||
- 所有操作都会记录在日志和系统事件日志中
|
|
||||||
- 支持通过任务计划程序进行管理和监控
|
|
||||||
|
|||||||
113
TEST_REPORT.md
113
TEST_REPORT.md
@@ -1,113 +0,0 @@
|
|||||||
# 智能网络监控脚本测试报告
|
|
||||||
|
|
||||||
## 测试日期
|
|
||||||
2025年6月6日
|
|
||||||
|
|
||||||
## 脚本概述
|
|
||||||
这是一个PowerShell智能网络监控脚本,用于监控指定IP的网络连通性,当网络持续中断超过设定时间后自动关闭计算机。
|
|
||||||
|
|
||||||
## 主要功能
|
|
||||||
1. **网络连通性监控** - 定期ping目标IP地址
|
|
||||||
2. **智能故障检测** - 区分临时网络波动和持续故障
|
|
||||||
3. **可配置监控窗口** - 防止因短暂网络中断导致误关机
|
|
||||||
4. **关机倒计时机制** - 提供最后的取消机会
|
|
||||||
5. **完整日志记录** - 记录所有操作和状态变化
|
|
||||||
6. **手动取消功能** - 用户可以随时按键取消关机
|
|
||||||
|
|
||||||
## 配置参数(原版)
|
|
||||||
- **目标IP**: 192.168.3.3
|
|
||||||
- **正常监控间隔**: 15秒
|
|
||||||
- **监控窗口**: 180秒(3分钟)
|
|
||||||
- **关机倒计时**: 60秒
|
|
||||||
- **倒计时ping间隔**: 3秒
|
|
||||||
- **Ping超时**: 3秒
|
|
||||||
|
|
||||||
## 测试配置(测试版)
|
|
||||||
- **目标IP**: 192.168.3.99(不存在的IP,模拟网络故障)
|
|
||||||
- **正常监控间隔**: 3秒
|
|
||||||
- **监控窗口**: 15秒
|
|
||||||
- **关机倒计时**: 10秒
|
|
||||||
- **倒计时ping间隔**: 2秒
|
|
||||||
|
|
||||||
## 修复的问题
|
|
||||||
1. **兼容性问题**: 修复了`Test-Connection`命令在旧版PowerShell中不支持`-TimeoutSeconds`参数的问题
|
|
||||||
2. **编码问题**: 将特殊Unicode字符(✓ ✗ ℹ)替换为ASCII字符,避免控制台显示乱码
|
|
||||||
|
|
||||||
## 测试结果
|
|
||||||
|
|
||||||
### ✅ 功能测试全部通过
|
|
||||||
|
|
||||||
#### 1. 脚本启动功能
|
|
||||||
- ✅ 正常启动和初始化
|
|
||||||
- ✅ 日志系统正常工作
|
|
||||||
- ✅ 配置参数正确加载
|
|
||||||
- ✅ 本地IP获取功能正常
|
|
||||||
|
|
||||||
#### 2. 网络监控功能
|
|
||||||
- ✅ 正常IP连通性检测(如8.8.8.8)
|
|
||||||
- ✅ 故障IP检测(如192.168.3.99)
|
|
||||||
- ✅ 网络异常处理机制
|
|
||||||
- ✅ Ping命令兼容性修复
|
|
||||||
|
|
||||||
#### 3. 监控窗口机制
|
|
||||||
- ✅ 首次网络中断检测
|
|
||||||
- ✅ 监控窗口计时功能
|
|
||||||
- ✅ 剩余时间计算和显示
|
|
||||||
- ✅ 监控窗口超时触发
|
|
||||||
|
|
||||||
#### 4. 关机倒计时功能
|
|
||||||
- ✅ 倒计时启动机制
|
|
||||||
- ✅ 倒计时期间网络快速检测
|
|
||||||
- ✅ 按键手动取消功能
|
|
||||||
- ✅ 模拟关机操作
|
|
||||||
|
|
||||||
#### 5. 日志记录功能
|
|
||||||
- ✅ 日志目录自动创建
|
|
||||||
- ✅ 按日期分类的日志文件
|
|
||||||
- ✅ 完整的操作记录
|
|
||||||
- ✅ 不同级别的日志分类(INFO、WARN、CRITICAL、FAIL、SUCCESS)
|
|
||||||
|
|
||||||
#### 6. 用户界面和显示
|
|
||||||
- ✅ 彩色控制台输出
|
|
||||||
- ✅ 清晰的状态提示
|
|
||||||
- ✅ 编码问题修复
|
|
||||||
- ✅ 用户友好的信息显示
|
|
||||||
|
|
||||||
## 实际测试执行记录
|
|
||||||
|
|
||||||
### 测试1(13:02:43开始)
|
|
||||||
- 监控窗口:15秒
|
|
||||||
- 实际触发时间:18秒后进入关机倒计时
|
|
||||||
- 关机倒计时:10秒
|
|
||||||
- 结果:正常完成模拟关机,重置状态继续监控
|
|
||||||
|
|
||||||
### 测试2(13:08:30开始)
|
|
||||||
- 同样配置重复测试
|
|
||||||
- 确认功能稳定性和一致性
|
|
||||||
- 编码问题已完全修复
|
|
||||||
|
|
||||||
## 实际使用建议
|
|
||||||
|
|
||||||
### 安全配置
|
|
||||||
1. **取消注释关机命令**:在`smart_shutdown.ps1`第217行,将`# Stop-Computer -Force`的注释去掉
|
|
||||||
2. **以管理员身份运行**:脚本需要管理员权限执行关机命令
|
|
||||||
3. **测试配置**:建议先用较短时间参数测试,确认无误后再使用正式配置
|
|
||||||
|
|
||||||
### 最佳实践
|
|
||||||
1. **监控窗口设置**:建议设置为3-5分钟,避免网络短暂波动造成误关机
|
|
||||||
2. **目标IP选择**:建议使用网关IP或重要服务器IP
|
|
||||||
3. **日志监控**:定期检查日志文件,了解网络状况
|
|
||||||
4. **备用方案**:考虑配置多个目标IP进行冗余检测
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
**脚本功能完整,测试全部通过,可以安全投入使用。**
|
|
||||||
|
|
||||||
经过全面测试,智能网络监控脚本各项功能均正常工作:
|
|
||||||
- 网络监控准确可靠
|
|
||||||
- 故障检测机制有效
|
|
||||||
- 用户交互友好
|
|
||||||
- 日志记录完整
|
|
||||||
- 编码问题已解决
|
|
||||||
- 兼容性问题已修复
|
|
||||||
|
|
||||||
脚本已准备好在生产环境中使用,能够有效监控网络连接并在必要时自动关闭计算机。
|
|
||||||
341
cmd/smart-shutdown/main.go
Normal file
341
cmd/smart-shutdown/main.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
|
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: "智能网络状态检测与自动关机后台服务",
|
||||||
|
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, "输出当前二进制内核版本并联网拉取发布树状态")
|
||||||
|
|
||||||
|
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.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: "废除并移除在册的后台服务、扫除环境关联残留",
|
||||||
|
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: "唤起执行守护后台",
|
||||||
|
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,519 +0,0 @@
|
|||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
智能网络监控脚本 - 系统级部署脚本
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
将脚本部署到系统级目录,配置为开机自启动,无需用户登录
|
|
||||||
|
|
||||||
部署位置:
|
|
||||||
- 程序文件: C:\Program Files\SmartNetworkMonitor\
|
|
||||||
- 配置文件: C:\ProgramData\SmartNetworkMonitor\
|
|
||||||
- 日志文件: C:\ProgramData\SmartNetworkMonitor\logs\
|
|
||||||
#>
|
|
||||||
|
|
||||||
# 定义系统级路径
|
|
||||||
$ProgramPath = "C:\Program Files\SmartNetworkMonitor"
|
|
||||||
$DataPath = "C:\ProgramData\SmartNetworkMonitor"
|
|
||||||
$LogPath = "$DataPath\logs"
|
|
||||||
|
|
||||||
Write-Host "==========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host "智能网络监控脚本 - 系统级部署" -ForegroundColor Cyan
|
|
||||||
Write-Host "==========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
Write-Host "部署计划:" -ForegroundColor Yellow
|
|
||||||
Write-Host "- 程序目录: $ProgramPath" -ForegroundColor White
|
|
||||||
Write-Host "- 数据目录: $DataPath" -ForegroundColor White
|
|
||||||
Write-Host "- 日志目录: $LogPath" -ForegroundColor White
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# 检查源文件
|
|
||||||
$SourcePath = $PSScriptRoot
|
|
||||||
$RequiredFiles = @(
|
|
||||||
"smart_shutdown.ps1"
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach ($file in $RequiredFiles) {
|
|
||||||
if (-not (Test-Path -Path (Join-Path $SourcePath $file))) {
|
|
||||||
Write-Error "找不到必需文件: $file"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "[OK] 源文件检查完成" -ForegroundColor Green
|
|
||||||
|
|
||||||
# 创建目录结构
|
|
||||||
Write-Host "正在创建目录结构..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
try {
|
|
||||||
# 创建程序目录
|
|
||||||
if (-not (Test-Path -Path $ProgramPath)) {
|
|
||||||
New-Item -Path $ProgramPath -ItemType Directory -Force | Out-Null
|
|
||||||
Write-Host "[OK] 创建程序目录: $ProgramPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建数据目录
|
|
||||||
if (-not (Test-Path -Path $DataPath)) {
|
|
||||||
New-Item -Path $DataPath -ItemType Directory -Force | Out-Null
|
|
||||||
Write-Host "[OK] 创建数据目录: $DataPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建日志目录
|
|
||||||
if (-not (Test-Path -Path $LogPath)) {
|
|
||||||
New-Item -Path $LogPath -ItemType Directory -Force | Out-Null
|
|
||||||
Write-Host "[OK] 创建日志目录: $LogPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "创建目录失败: $($_.Exception.Message)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建系统优化版的主脚本
|
|
||||||
Write-Host "正在创建系统版本的脚本..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
$SystemScriptContent = @"
|
|
||||||
#Requires -Version 5.1
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
智能网络监控脚本 - 系统服务版本
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
系统级网络监控脚本,在网络持续中断时自动关机
|
|
||||||
|
|
||||||
特点:
|
|
||||||
- 开机自启动,无需用户登录
|
|
||||||
- 日志存储在系统数据目录
|
|
||||||
- 优化的错误处理和权限管理
|
|
||||||
#>
|
|
||||||
|
|
||||||
# ==================== 系统级配置参数 ====================
|
|
||||||
|
|
||||||
# 监控的目标IP地址
|
|
||||||
[string]`$TargetIP = "192.168.3.3"
|
|
||||||
|
|
||||||
# 正常监控时的ping间隔(秒)
|
|
||||||
[int]`$NormalPingInterval = 15
|
|
||||||
|
|
||||||
# 监控窗口时长(秒) - 在此期间持续失败则触发关机
|
|
||||||
[int]`$MonitorWindowSeconds = 180
|
|
||||||
|
|
||||||
# 关机倒计时时长(秒)
|
|
||||||
[int]`$ShutdownCountdown = 60
|
|
||||||
|
|
||||||
# 关机倒计时阶段的ping间隔(秒)
|
|
||||||
[int]`$CountdownPingInterval = 3
|
|
||||||
|
|
||||||
# 系统级日志配置
|
|
||||||
[string]`$LogDirectory = "$LogPath"
|
|
||||||
[string]`$LogFile = Join-Path -Path `$LogDirectory -ChildPath "network_monitor_`$(Get-Date -Format 'yyyyMMdd').log"
|
|
||||||
[string]`$ConfigFile = "$DataPath\config.json"
|
|
||||||
[int]`$MaxLogDays = 30
|
|
||||||
|
|
||||||
# 正常连接时的日志记录间隔计数器
|
|
||||||
[int]`$normalLogInterval = 24 # 每24次循环记录一次状态 (约6分钟)
|
|
||||||
[int]`$normalLogCounter = 0
|
|
||||||
|
|
||||||
# =================================================
|
|
||||||
|
|
||||||
# --- 函数定义 ---
|
|
||||||
|
|
||||||
# 加载配置文件
|
|
||||||
function Load-Configuration {
|
|
||||||
if (Test-Path -Path `$ConfigFile) {
|
|
||||||
try {
|
|
||||||
`$config = Get-Content -Path `$ConfigFile | ConvertFrom-Json
|
|
||||||
if (`$config.TargetIP) { `$script:TargetIP = `$config.TargetIP }
|
|
||||||
if (`$config.MonitorWindowSeconds) { `$script:MonitorWindowSeconds = `$config.MonitorWindowSeconds }
|
|
||||||
if (`$config.ShutdownCountdown) { `$script:ShutdownCountdown = `$config.ShutdownCountdown }
|
|
||||||
if (`$config.NormalPingInterval) { `$script:NormalPingInterval = `$config.NormalPingInterval }
|
|
||||||
Write-Log -Level "INFO" -Message "配置文件加载成功"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "WARN" -Message "配置文件格式错误,使用默认配置"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# 创建默认配置文件
|
|
||||||
`$defaultConfig = @{
|
|
||||||
TargetIP = `$TargetIP
|
|
||||||
MonitorWindowSeconds = `$MonitorWindowSeconds
|
|
||||||
ShutdownCountdown = `$ShutdownCountdown
|
|
||||||
NormalPingInterval = `$NormalPingInterval
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
`$defaultConfig | ConvertTo-Json | Set-Content -Path `$ConfigFile
|
|
||||||
Write-Log -Level "INFO" -Message "创建默认配置文件: `$ConfigFile"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "WARN" -Message "无法创建配置文件,使用内置默认值"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 日志写入函数
|
|
||||||
function Write-Log {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=`$true)]
|
|
||||||
[ValidateSet("INFO", "SUCCESS", "FAIL", "WARN", "CRITICAL", "DEBUG")]
|
|
||||||
[string]`$Level,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=`$true)]
|
|
||||||
[string]`$Message
|
|
||||||
)
|
|
||||||
|
|
||||||
`$logTimestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
||||||
`$logEntry = "[`$logTimestamp] [`$Level] `$Message"
|
|
||||||
|
|
||||||
# 尝试写入日志文件
|
|
||||||
try {
|
|
||||||
if (-not (Test-Path -Path `$LogDirectory)) {
|
|
||||||
New-Item -Path `$LogDirectory -ItemType Directory -ErrorAction Stop | Out-Null
|
|
||||||
}
|
|
||||||
Add-Content -Path `$LogFile -Value `$logEntry -ErrorAction Stop
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
# 如果写入失败,尝试输出到事件日志
|
|
||||||
try {
|
|
||||||
if (-not [System.Diagnostics.EventLog]::SourceExists("SmartNetworkMonitor")) {
|
|
||||||
New-EventLog -LogName "Application" -Source "SmartNetworkMonitor"
|
|
||||||
}
|
|
||||||
Write-EventLog -LogName "Application" -Source "SmartNetworkMonitor" -EventId 1001 -EntryType Information -Message `$logEntry
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
# 最后尝试输出到控制台(虽然在服务模式下不可见)
|
|
||||||
Write-Host "[`$logTimestamp] [CRITICAL] 日志写入失败!`$Message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 在服务模式下,也输出到事件日志作为备份
|
|
||||||
try {
|
|
||||||
if (`$Level -eq "CRITICAL" -or `$Level -eq "FAIL") {
|
|
||||||
Write-EventLog -LogName "Application" -Source "SmartNetworkMonitor" -EventId 1002 -EntryType Warning -Message `$logEntry -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
# 忽略事件日志写入错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 清理旧日志文件
|
|
||||||
function Remove-OldLogs {
|
|
||||||
try {
|
|
||||||
`$cutoffDate = (Get-Date).AddDays(-`$MaxLogDays)
|
|
||||||
`$oldLogs = Get-ChildItem -Path `$LogDirectory -Filter "network_monitor_*.log" | Where-Object { `$_.LastWriteTime -lt `$cutoffDate }
|
|
||||||
foreach (`$oldLog in `$oldLogs) {
|
|
||||||
Remove-Item -Path `$oldLog.FullName -Force
|
|
||||||
Write-Log -Level "INFO" -Message "删除旧日志文件: `$(`$oldLog.Name)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "WARN" -Message "清理旧日志文件时出错: `$(`$_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# =================================================
|
|
||||||
|
|
||||||
# --- 主程序开始 ---
|
|
||||||
|
|
||||||
# 初始化
|
|
||||||
Load-Configuration
|
|
||||||
Remove-OldLogs
|
|
||||||
|
|
||||||
# 获取系统信息
|
|
||||||
try {
|
|
||||||
`$runningAs = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
||||||
`$localIP = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { `$_.InterfaceAlias -notmatch "Loopback" } | Select-Object -First 1).IPAddress
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
`$runningAs = "Unknown"
|
|
||||||
`$localIP = "Unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "INFO" -Message "========== 系统级网络监控脚本启动 =========="
|
|
||||||
Write-Log -Level "INFO" -Message "运行账户: `$runningAs"
|
|
||||||
Write-Log -Level "INFO" -Message "本机IP: `$localIP"
|
|
||||||
Write-Log -Level "INFO" -Message "监控目标: `$TargetIP"
|
|
||||||
Write-Log -Level "INFO" -Message "监控窗口: `${MonitorWindowSeconds}秒"
|
|
||||||
Write-Log -Level "INFO" -Message "关机倒计时: `${ShutdownCountdown}秒"
|
|
||||||
Write-Log -Level "INFO" -Message "日志目录: `$LogDirectory"
|
|
||||||
Write-Log -Level "INFO" -Message "配置文件: `$ConfigFile"
|
|
||||||
|
|
||||||
# 主循环
|
|
||||||
`$failureStartTime = `$null
|
|
||||||
|
|
||||||
while (`$true) {
|
|
||||||
# 测试网络连接
|
|
||||||
try {
|
|
||||||
`$pingResult = Test-Connection -ComputerName `$TargetIP -Count 1 -Quiet -ErrorAction SilentlyContinue
|
|
||||||
`$isOnline = `$pingResult
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "FAIL" -Message "网络测试出现异常: `$(`$_.Exception.Message)"
|
|
||||||
`$isOnline = `$false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (`$isOnline) {
|
|
||||||
if (`$failureStartTime) {
|
|
||||||
Write-Log -Level "SUCCESS" -Message "网络连接已恢复。"
|
|
||||||
`$failureStartTime = `$null
|
|
||||||
} else {
|
|
||||||
# 减少正常连接时的日志频率
|
|
||||||
`$normalLogCounter++
|
|
||||||
if (`$normalLogCounter -ge `$normalLogInterval) {
|
|
||||||
Write-Log -Level "INFO" -Message "网络连接正常 (定期状态报告)"
|
|
||||||
`$normalLogCounter = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds `$NormalPingInterval
|
|
||||||
|
|
||||||
} else {
|
|
||||||
# 网络不通时的处理逻辑
|
|
||||||
Write-Log -Level "FAIL" -Message "Ping失败 - 目标: `$TargetIP"
|
|
||||||
|
|
||||||
if (-not `$failureStartTime) {
|
|
||||||
`$failureStartTime = Get-Date
|
|
||||||
Write-Log -Level "WARN" -Message "网络首次中断,开始进入监控窗口计时。"
|
|
||||||
}
|
|
||||||
|
|
||||||
`$failureDuration = (New-TimeSpan -Start `$failureStartTime -End (Get-Date)).TotalSeconds
|
|
||||||
Write-Log -Level "INFO" -Message "网络已持续中断 `$([math]::Round(`$failureDuration)) / `${MonitorWindowSeconds} 秒。"
|
|
||||||
|
|
||||||
if (`$failureDuration -ge `$MonitorWindowSeconds) {
|
|
||||||
Write-Log -Level "CRITICAL" -Message "网络持续中断已超过 `${MonitorWindowSeconds} 秒,开始关机流程。"
|
|
||||||
|
|
||||||
`$shutdownCancelled = `$false
|
|
||||||
`$countdownEndTime = (Get-Date).AddSeconds(`$ShutdownCountdown)
|
|
||||||
|
|
||||||
while ((Get-Date) -lt `$countdownEndTime -and -not `$shutdownCancelled) {
|
|
||||||
`$remainingSeconds = [math]::Ceiling((`$countdownEndTime - (Get-Date)).TotalSeconds)
|
|
||||||
if (`$remainingSeconds -le 0) { break }
|
|
||||||
|
|
||||||
Write-Log -Level "WARN" -Message "距离关机还有 `$remainingSeconds 秒... 正在快速检测网络。"
|
|
||||||
|
|
||||||
# 在倒计时中再次检测网络
|
|
||||||
try {
|
|
||||||
if (Test-Connection -ComputerName `$TargetIP -Count 1 -Quiet -ErrorAction SilentlyContinue) {
|
|
||||||
Write-Log -Level "SUCCESS" -Message "网络在倒计时期间恢复!取消关机。"
|
|
||||||
`$failureStartTime = `$null
|
|
||||||
`$shutdownCancelled = `$true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "WARN" -Message "倒计时期间网络测试出现异常: `$(`$_.Exception.Message)"
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds `$CountdownPingInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not `$shutdownCancelled) {
|
|
||||||
Write-Log -Level "CRITICAL" -Message "关机倒计时完成,执行系统关机命令。"
|
|
||||||
try {
|
|
||||||
# 执行实际关机命令
|
|
||||||
Stop-Computer -Force
|
|
||||||
Write-Log -Level "CRITICAL" -Message "关机命令已执行。"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "CRITICAL" -Message "关机命令执行失败: `$(`$_.Exception.Message)"
|
|
||||||
}
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Start-Sleep -Seconds `$NormalPingInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"@
|
|
||||||
|
|
||||||
# 写入系统版本脚本
|
|
||||||
$SystemScriptPath = Join-Path $ProgramPath "smart_shutdown_system.ps1"
|
|
||||||
try {
|
|
||||||
$SystemScriptContent | Set-Content -Path $SystemScriptPath -Encoding UTF8
|
|
||||||
Write-Host "[OK] 创建系统脚本: $SystemScriptPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "创建系统脚本失败: $($_.Exception.Message)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建默认配置文件
|
|
||||||
Write-Host "正在创建配置文件..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
$DefaultConfig = @{
|
|
||||||
TargetIP = "192.168.3.3"
|
|
||||||
MonitorWindowSeconds = 180
|
|
||||||
ShutdownCountdown = 60
|
|
||||||
NormalPingInterval = 15
|
|
||||||
}
|
|
||||||
|
|
||||||
$ConfigPath = Join-Path $DataPath "config.json"
|
|
||||||
try {
|
|
||||||
$DefaultConfig | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding UTF8
|
|
||||||
Write-Host "[OK] 创建配置文件: $ConfigPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "创建配置文件失败: $($_.Exception.Message)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 配置任务计划程序
|
|
||||||
Write-Host "正在配置任务计划程序..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
try {
|
|
||||||
# 删除已存在的任务(如果有)
|
|
||||||
try {
|
|
||||||
Unregister-ScheduledTask -TaskName "Smart Network Shutdown Monitor" -Confirm:$false -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
# 忽略错误,任务可能不存在
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建触发器(系统启动时)
|
|
||||||
$Trigger = New-ScheduledTaskTrigger -AtStartup
|
|
||||||
|
|
||||||
# 创建操作
|
|
||||||
$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$SystemScriptPath`""
|
|
||||||
|
|
||||||
# 创建设置
|
|
||||||
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd -ExecutionTimeLimit (New-TimeSpan -Days 365)
|
|
||||||
|
|
||||||
# 创建主体(以SYSTEM身份运行)
|
|
||||||
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
||||||
|
|
||||||
# 注册任务
|
|
||||||
Register-ScheduledTask -TaskName "Smart Network Shutdown Monitor" -Trigger $Trigger -Action $Action -Settings $Settings -Principal $Principal -Description "智能网络监控脚本 - 系统级运行"
|
|
||||||
|
|
||||||
Write-Host "[OK] 任务计划程序配置完成" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "配置任务计划程序失败: $($_.Exception.Message)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 创建管理脚本
|
|
||||||
Write-Host "正在创建管理工具..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
$ManagementScript = @"
|
|
||||||
# 智能网络监控脚本 - 管理工具
|
|
||||||
|
|
||||||
Write-Host "智能网络监控脚本 - 管理工具" -ForegroundColor Cyan
|
|
||||||
Write-Host "==============================" -ForegroundColor Cyan
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
Write-Host "可用操作:" -ForegroundColor Yellow
|
|
||||||
Write-Host "1. 查看任务状态"
|
|
||||||
Write-Host "2. 启动监控任务"
|
|
||||||
Write-Host "3. 停止监控任务"
|
|
||||||
Write-Host "4. 查看今天的日志"
|
|
||||||
Write-Host "5. 查看实时日志"
|
|
||||||
Write-Host "6. 查看配置文件"
|
|
||||||
Write-Host "7. 编辑配置文件"
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
`$choice = Read-Host "请选择操作 (1-7)"
|
|
||||||
|
|
||||||
switch (`$choice) {
|
|
||||||
"1" {
|
|
||||||
Get-ScheduledTask -TaskName "Smart Network Shutdown Monitor" | Format-Table
|
|
||||||
}
|
|
||||||
"2" {
|
|
||||||
Start-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
Write-Host "任务已启动" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
"3" {
|
|
||||||
Stop-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
Write-Host "任务已停止" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
"4" {
|
|
||||||
`$logFile = "$LogPath\network_monitor_`$(Get-Date -Format 'yyyyMMdd').log"
|
|
||||||
if (Test-Path `$logFile) {
|
|
||||||
Get-Content `$logFile
|
|
||||||
} else {
|
|
||||||
Write-Host "今天的日志文件不存在" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"5" {
|
|
||||||
`$logFile = "$LogPath\network_monitor_`$(Get-Date -Format 'yyyyMMdd').log"
|
|
||||||
if (Test-Path `$logFile) {
|
|
||||||
Get-Content `$logFile -Wait
|
|
||||||
} else {
|
|
||||||
Write-Host "今天的日志文件不存在" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"6" {
|
|
||||||
Get-Content "$DataPath\config.json"
|
|
||||||
}
|
|
||||||
"7" {
|
|
||||||
notepad "$DataPath\config.json"
|
|
||||||
}
|
|
||||||
default {
|
|
||||||
Write-Host "无效选择" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "常用命令:" -ForegroundColor Yellow
|
|
||||||
Write-Host "1. 查看任务状态:"
|
|
||||||
Write-Host " Get-ScheduledTask -TaskName 'Smart Network Shutdown Monitor'"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "2. 启动任务:"
|
|
||||||
Write-Host " Start-ScheduledTask -TaskName 'Smart Network Shutdown Monitor'"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "3. 停止任务:"
|
|
||||||
Write-Host " Stop-ScheduledTask -TaskName 'Smart Network Shutdown Monitor'"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "4. 查看今天日志:"
|
|
||||||
Write-Host " Get-Content '$LogPath\network_monitor_`$(Get-Date -Format 'yyyyMMdd').log'"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "5. 实时查看日志:"
|
|
||||||
Write-Host " Get-Content '$LogPath\network_monitor_`$(Get-Date -Format 'yyyyMMdd').log' -Wait"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "6. 查看配置:"
|
|
||||||
Write-Host " Get-Content '$DataPath\config.json'"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "7. 编辑配置文件:"
|
|
||||||
Write-Host " notepad '$DataPath\config.json'"
|
|
||||||
Write-Host ""
|
|
||||||
"@
|
|
||||||
|
|
||||||
$ManagementScriptPath = Join-Path $ProgramPath "manage.ps1"
|
|
||||||
try {
|
|
||||||
$ManagementScript | Set-Content -Path $ManagementScriptPath -Encoding UTF8
|
|
||||||
Write-Host "[OK] 创建管理脚本: $ManagementScriptPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Error "创建管理脚本失败: $($_.Exception.Message)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "[INFO] 系统级部署完成!" -ForegroundColor Green
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "部署摘要:" -ForegroundColor Cyan
|
|
||||||
Write-Host "[OK] 程序文件已部署到: $ProgramPath" -ForegroundColor White
|
|
||||||
Write-Host "[OK] 数据目录已创建: $DataPath" -ForegroundColor White
|
|
||||||
Write-Host "[OK] 日志目录已创建: $LogPath" -ForegroundColor White
|
|
||||||
Write-Host "[OK] 任务计划程序已配置(系统启动时运行)" -ForegroundColor White
|
|
||||||
Write-Host "[OK] SYSTEM 权限配置完成" -ForegroundColor White
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "管理工具:" -ForegroundColor Yellow
|
|
||||||
Write-Host "- 管理脚本: $ManagementScriptPath" -ForegroundColor White
|
|
||||||
Write-Host "- 配置文件: $DataPath\config.json" -ForegroundColor White
|
|
||||||
Write-Host "- 今天日志: $LogPath\network_monitor_$(Get-Date -Format 'yyyyMMdd').log" -ForegroundColor White
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "下次重启后,脚本将自动运行!" -ForegroundColor Green
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
# 询问是否立即启动
|
|
||||||
$response = Read-Host "是否立即启动监控任务?(Y/N)"
|
|
||||||
if ($response -eq 'Y' -or $response -eq 'y' -or $response -eq '') {
|
|
||||||
try {
|
|
||||||
Start-ScheduledTask -TaskName "Smart Network Shutdown Monitor"
|
|
||||||
Write-Host "[OK] 监控任务已启动" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[X] 启动任务失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
go.mod
Normal file
23
go.mod
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module smart-shutdown
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.19.0
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
golang.org/x/net v0.49.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
)
|
||||||
33
go.sum
Normal file
33
go.sum
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
|
||||||
|
github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/prometheus-community/pro-bing v0.8.0 h1:CEY/g1/AgERRDjxw5P32ikcOgmrSuXs7xon7ovx6mNc=
|
||||||
|
github.com/prometheus-community/pro-bing v0.8.0/go.mod h1:Idyxz8raDO6TgkUN6ByiEGvWJNyQd40kN9ZUeho3lN0=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
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=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
115
pkg/config/config.go
Normal file
115
pkg/config/config.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
TargetIP string `json:"TargetIP"`
|
||||||
|
MonitorWindowSeconds int `json:"MonitorWindowSeconds"`
|
||||||
|
ShutdownCountdown int `json:"ShutdownCountdown"`
|
||||||
|
NormalPingInterval int `json:"NormalPingInterval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
TargetIP: "192.168.3.3",
|
||||||
|
MonitorWindowSeconds: 180,
|
||||||
|
ShutdownCountdown: 60,
|
||||||
|
NormalPingInterval: 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigDir() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return `C:\ProgramData\SmartNetworkMonitor`
|
||||||
|
}
|
||||||
|
return `/etc/smart-network-monitor`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogDir() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return `C:\ProgramData\SmartNetworkMonitor\logs`
|
||||||
|
}
|
||||||
|
return `/var/log/smart-network-monitor`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
configDir := GetConfigDir()
|
||||||
|
configPath := filepath.Join(configDir, "config.json")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return DefaultConfig(), nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
|
||||||
|
data = data[3:]
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
if err := json.Unmarshal(data, cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
60
pkg/daemon/service.go
Normal file
60
pkg/daemon/service.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"smart-shutdown/pkg/config"
|
||||||
|
"smart-shutdown/pkg/logger"
|
||||||
|
"smart-shutdown/pkg/monitor"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type program struct {
|
||||||
|
exit chan struct{}
|
||||||
|
cancel context.CancelFunc
|
||||||
|
cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *program) Start(s service.Service) error {
|
||||||
|
logger.Info("启动系统监控后台服务")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
p.cancel = cancel
|
||||||
|
p.exit = make(chan struct{})
|
||||||
|
|
||||||
|
go p.run(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *program) run(ctx context.Context) {
|
||||||
|
monitor.Run(ctx, p.cfg)
|
||||||
|
close(p.exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *program) Stop(s service.Service) error {
|
||||||
|
logger.Info("停止系统监控后台服务")
|
||||||
|
if p.cancel != nil {
|
||||||
|
p.cancel()
|
||||||
|
}
|
||||||
|
<-p.exit
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetService(cfg *config.Config) (service.Service, error) {
|
||||||
|
svcConfig := &service.Config{
|
||||||
|
Name: "SmartNetworkMonitor",
|
||||||
|
DisplayName: "Smart Network Shutdown Monitor",
|
||||||
|
Description: "A reliable daemon that periodically monitors network states and triggers node suspension logically.",
|
||||||
|
}
|
||||||
|
|
||||||
|
prg := &program{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := service.New(prg, svcConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
89
pkg/logger/logger.go
Normal file
89
pkg/logger/logger.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
"smart-shutdown/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fileLogger *lumberjack.Logger
|
||||||
|
|
||||||
|
func InitLogger() error {
|
||||||
|
logDir := config.GetLogDir()
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
logDir = "logs"
|
||||||
|
os.MkdirAll(logDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
logFile := filepath.Join(logDir, "network_monitor.log")
|
||||||
|
|
||||||
|
fileLogger = &lumberjack.Logger{
|
||||||
|
Filename: logFile,
|
||||||
|
MaxSize: 10,
|
||||||
|
MaxBackups: 30,
|
||||||
|
MaxAge: 30,
|
||||||
|
Compress: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLog(level, plainPrefix, format string, v ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, v...)
|
||||||
|
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
||||||
|
|
||||||
|
if fileLogger != nil {
|
||||||
|
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...)
|
||||||
|
}
|
||||||
107
pkg/monitor/statemachine.go
Normal file
107
pkg/monitor/statemachine.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"smart-shutdown/pkg/config"
|
||||||
|
"smart-shutdown/pkg/logger"
|
||||||
|
"smart-shutdown/pkg/pinger"
|
||||||
|
"smart-shutdown/pkg/system"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(ctx context.Context, cfg *config.Config) {
|
||||||
|
logger.Info("启动后台网络监控")
|
||||||
|
logger.Info("监控目标 IP: %s", cfg.TargetIP)
|
||||||
|
logger.Info("故障判定窗口: %d秒", cfg.MonitorWindowSeconds)
|
||||||
|
logger.Info("关机倒计时阈值: %d秒", cfg.ShutdownCountdown)
|
||||||
|
|
||||||
|
var failureStartTime time.Time
|
||||||
|
var inFailureWindow bool = false
|
||||||
|
|
||||||
|
normalStatusCounter := 0
|
||||||
|
const normalStatusLogInterval = 24
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Info("接收到系统停止指令, 退出核心调度监控")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
isOnline := pinger.Ping(cfg.TargetIP, 3)
|
||||||
|
|
||||||
|
if isOnline {
|
||||||
|
if inFailureWindow {
|
||||||
|
logger.Succ("网络连通性恢复, 监控窗口重置")
|
||||||
|
inFailureWindow = false
|
||||||
|
normalStatusCounter = 0
|
||||||
|
} else {
|
||||||
|
normalStatusCounter++
|
||||||
|
if normalStatusCounter >= normalStatusLogInterval {
|
||||||
|
logger.Info("网络探测状态正常 (定期状态更新)")
|
||||||
|
normalStatusCounter = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(cfg.NormalPingInterval) * time.Second)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.Warn("目标节点未响应探针包: %s", cfg.TargetIP)
|
||||||
|
|
||||||
|
if !inFailureWindow {
|
||||||
|
failureStartTime = time.Now()
|
||||||
|
inFailureWindow = true
|
||||||
|
logger.Warn("触发网络状态失效, 开始累计中断时间监控")
|
||||||
|
}
|
||||||
|
|
||||||
|
failureDuration := time.Since(failureStartTime).Seconds()
|
||||||
|
|
||||||
|
if int(failureDuration) >= cfg.MonitorWindowSeconds {
|
||||||
|
logger.Crit("持续中断时长超越设定的阈值上限 (%d秒), 进入关机响应流程", cfg.MonitorWindowSeconds)
|
||||||
|
|
||||||
|
shutdownCancelled := startCountdown(ctx, cfg.TargetIP, cfg.ShutdownCountdown)
|
||||||
|
|
||||||
|
if !shutdownCancelled {
|
||||||
|
logger.Crit("倒计时时限完毕, 开始下发系统级级终态关机指令")
|
||||||
|
system.ExecuteShutdown()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
inFailureWindow = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Info("网络持续断失: %.0f/%d 秒", failureDuration, cfg.MonitorWindowSeconds)
|
||||||
|
time.Sleep(time.Duration(cfg.NormalPingInterval) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCountdown(ctx context.Context, targetIP string, countdownSec int) bool {
|
||||||
|
logger.Warn("执行系统关机倒计时流程 (预设: %d秒)...", countdownSec)
|
||||||
|
|
||||||
|
endTime := time.Now().Add(time.Duration(countdownSec) * time.Second)
|
||||||
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
remaining := int(time.Until(endTime).Seconds())
|
||||||
|
if remaining <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warn("距离关机指令处分剩余: %d 秒", remaining)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Info("挂起关机倒计流程:收到系统强制干预信号")
|
||||||
|
return true
|
||||||
|
case <-ticker.C:
|
||||||
|
if pinger.Ping(targetIP, 3) {
|
||||||
|
logger.Succ("倒计时周期内网络指标恢复, 全局关机流程已被重置并取消")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
pkg/pinger/pinger.go
Normal file
34
pkg/pinger/pinger.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package pinger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
probing "github.com/prometheus-community/pro-bing"
|
||||||
|
"smart-shutdown/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Ping(targetIP string, timeoutSec int) bool {
|
||||||
|
pinger, err := probing.NewPinger(targetIP)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("目标 IP 解析异常 [%s]: %v", targetIP, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pinger.SetPrivileged(true)
|
||||||
|
|
||||||
|
pinger.Count = 1
|
||||||
|
pinger.Timeout = time.Duration(timeoutSec) * time.Second
|
||||||
|
|
||||||
|
err = pinger.Run()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fail("探针执行失败: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := pinger.Statistics()
|
||||||
|
if stats.PacketsRecv > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
26
pkg/system/shutdown.go
Normal file
26
pkg/system/shutdown.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"smart-shutdown/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExecuteShutdown() error {
|
||||||
|
logger.Crit("系统调用:执行关机指令")
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd = exec.Command("shutdown", "-s", "-f", "-t", "0")
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("shutdown", "-h", "now")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
logger.Crit("系统关机指令执行失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
#Requires -Version 5.1
|
|
||||||
#Requires -RunAsAdministrator
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
一个智能网络监控脚本,当检测到与目标IP的网络持续中断后,会自动关闭计算机。
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
该脚本会定期测试与指定目标IP的网络连通性。
|
|
||||||
如果在设定的“监控窗口”时间内连接持续失败,脚本将启动一个可取消的关机倒计时。
|
|
||||||
如果在倒计时期间网络恢复,关机将被中止。
|
|
||||||
所有操作都会被记录在日志文件中。
|
|
||||||
#>
|
|
||||||
|
|
||||||
# ==================== 配置参数 ====================
|
|
||||||
|
|
||||||
# 监控的目标IP地址
|
|
||||||
[string]$TargetIP = "192.168.3.3"
|
|
||||||
|
|
||||||
# 正常监控时的ping间隔(秒)
|
|
||||||
[int]$NormalPingInterval = 15
|
|
||||||
|
|
||||||
# 监控窗口时长(秒) - 在此期间持续失败则触发关机
|
|
||||||
[int]$MonitorWindowSeconds = 60
|
|
||||||
|
|
||||||
# 关机倒计时时长(秒)
|
|
||||||
[int]$ShutdownCountdown = 60
|
|
||||||
|
|
||||||
# 关机倒计时阶段的ping间隔(秒)
|
|
||||||
[int]$CountdownPingInterval = 3
|
|
||||||
|
|
||||||
# ping超时时间(秒)
|
|
||||||
[int]$PingTimeout = 3
|
|
||||||
|
|
||||||
# 日志配置
|
|
||||||
[string]$LogDirectory = Join-Path -Path $PSScriptRoot -ChildPath "logs"
|
|
||||||
[string]$LogFile = Join-Path -Path $LogDirectory -ChildPath "network_monitor_$(Get-Date -Format 'yyyyMMdd').log"
|
|
||||||
[int]$MaxLogDays = 30
|
|
||||||
|
|
||||||
# =================================================
|
|
||||||
|
|
||||||
# --- 函数定义 ---
|
|
||||||
|
|
||||||
# 日志写入函数
|
|
||||||
function Write-Log {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[ValidateSet("INFO", "SUCCESS", "FAIL", "WARN", "CRITICAL", "DEBUG")]
|
|
||||||
[string]$Level,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$Message
|
|
||||||
)
|
|
||||||
|
|
||||||
$logTimestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
||||||
$logEntry = "[$logTimestamp] [$Level] $Message"
|
|
||||||
|
|
||||||
# 尝试写入日志文件
|
|
||||||
try {
|
|
||||||
if (-not (Test-Path -Path $LogDirectory)) {
|
|
||||||
New-Item -Path $LogDirectory -ItemType Directory -ErrorAction Stop | Out-Null
|
|
||||||
}
|
|
||||||
Add-Content -Path $LogFile -Value $logEntry -ErrorAction Stop
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[$logTimestamp] [CRITICAL] 日志文件写入失败!请检查权限或磁盘空间。" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# 根据日志级别在控制台输出不同颜色的信息
|
|
||||||
switch ($Level) {
|
|
||||||
"SUCCESS" { Write-Host $logEntry -ForegroundColor Green }
|
|
||||||
"FAIL" { Write-Host $logEntry -ForegroundColor Red }
|
|
||||||
"WARN" { Write-Host $logEntry -ForegroundColor Yellow }
|
|
||||||
"CRITICAL" { Write-Host $logEntry -ForegroundColor DarkRed }
|
|
||||||
default { Write-Host $logEntry }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 获取本机的最佳IP地址
|
|
||||||
function Get-LocalIP {
|
|
||||||
param($TargetIP)
|
|
||||||
try {
|
|
||||||
# 尝试寻找与目标IP在同一个子网的本机IP地址
|
|
||||||
$targetSubnet = ($TargetIP.Split('.')[0..2]) -join '.'
|
|
||||||
$ip = Get-NetIPAddress -AddressFamily IPv4 -AddressState Preferred | Where-Object { $_.IPAddress -like "$targetSubnet.*" } | Select-Object -First 1 -ExpandProperty IPAddress
|
|
||||||
|
|
||||||
if ($ip) { return $ip }
|
|
||||||
|
|
||||||
# 如果找不到,则返回任意一个非环回的私有IP地址
|
|
||||||
$ip = Get-NetIPAddress -AddressFamily IPv4 -AddressState Preferred | Where-Object { $_.IPAddress -notlike "127.0.0.1" -and $_.InterfaceAlias -notlike "Loopback*" } | Select-Object -First 1 -ExpandProperty IPAddress
|
|
||||||
return $ip
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return "未知"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# --- 脚本主逻辑 ---
|
|
||||||
|
|
||||||
# 清理旧日志
|
|
||||||
if (Test-Path -Path $LogDirectory) {
|
|
||||||
Get-ChildItem -Path $LogDirectory -Filter "*.log" | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$MaxLogDays) } | Remove-Item
|
|
||||||
}
|
|
||||||
|
|
||||||
# 初始化
|
|
||||||
Clear-Host
|
|
||||||
$localIP = Get-LocalIP -TargetIP $TargetIP
|
|
||||||
Write-Log -Level "INFO" -Message "========== 脚本启动 =========="
|
|
||||||
Write-Log -Level "INFO" -Message "本机IP: $localIP"
|
|
||||||
Write-Log -Level "INFO" -Message "监控目标: $TargetIP"
|
|
||||||
Write-Log -Level "INFO" -Message "监控窗口: ${MonitorWindowSeconds}秒"
|
|
||||||
Write-Log -Level "INFO" -Message "关机倒计时: ${ShutdownCountdown}秒"
|
|
||||||
Write-Log -Level "INFO" -Message "日志文件: $LogFile"
|
|
||||||
Write-Log -Level "INFO" -Message "脚本已启动,按 Ctrl+C 退出..."
|
|
||||||
Write-Host "========================================"
|
|
||||||
|
|
||||||
# 主循环
|
|
||||||
$failureStartTime = $null
|
|
||||||
$normalStatusCounter = 0 # 正常状态计数器,用于定期记录正常运行日志
|
|
||||||
$normalStatusLogInterval = 24 # 每24次正常检测记录一次日志(约6分钟)
|
|
||||||
|
|
||||||
while ($true) {# 测试网络连接
|
|
||||||
try {
|
|
||||||
# 使用兼容性更好的ping方法
|
|
||||||
$pingResult = Test-Connection -ComputerName $TargetIP -Count 1 -Quiet -ErrorAction SilentlyContinue
|
|
||||||
$isOnline = $pingResult
|
|
||||||
} catch {
|
|
||||||
Write-Log -Level "FAIL" -Message "网络测试出现异常: $($_.Exception.Message)"
|
|
||||||
$isOnline = $false
|
|
||||||
} if ($isOnline) {
|
|
||||||
if ($failureStartTime) {
|
|
||||||
Write-Log -Level "SUCCESS" -Message "网络连接已恢复。"
|
|
||||||
Write-Host "[OK] 网络连接已恢复,重置监控窗口" -ForegroundColor Green
|
|
||||||
$failureStartTime = $null # 重置失败计时器
|
|
||||||
$normalStatusCounter = 0 # 重置正常状态计数器
|
|
||||||
} else {
|
|
||||||
Write-Host "[OK] 网络连接正常" -ForegroundColor Green
|
|
||||||
# 正常连接时减少日志频率,只定期记录
|
|
||||||
$normalStatusCounter++
|
|
||||||
if ($normalStatusCounter -ge $normalStatusLogInterval) {
|
|
||||||
Write-Log -Level "INFO" -Message "网络连接持续正常(已检测 $normalStatusCounter 次)"
|
|
||||||
$normalStatusCounter = 0 # 重置计数器
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# 只在控制台显示等待信息,不写入日志文件
|
|
||||||
Write-Host "等待 ${NormalPingInterval}秒后继续监控..." -ForegroundColor Cyan
|
|
||||||
Start-Sleep -Seconds $NormalPingInterval
|
|
||||||
|
|
||||||
} else {
|
|
||||||
# 如果网络不通
|
|
||||||
Write-Log -Level "FAIL" -Message "Ping失败 - 目标: $TargetIP"
|
|
||||||
|
|
||||||
if (-not $failureStartTime) {
|
|
||||||
# 记录首次失败的时间
|
|
||||||
$failureStartTime = Get-Date
|
|
||||||
Write-Log -Level "WARN" -Message "网络首次中断,开始进入监控窗口计时。"
|
|
||||||
} # 计算网络已中断的时间
|
|
||||||
$failureDuration = (New-TimeSpan -Start $failureStartTime -End (Get-Date)).TotalSeconds
|
|
||||||
Write-Log -Level "INFO" -Message "网络已持续中断 $([math]::Round($failureDuration)) / ${MonitorWindowSeconds} 秒。"
|
|
||||||
|
|
||||||
# 显示详细状态
|
|
||||||
$remainingTime = $MonitorWindowSeconds - [math]::Round($failureDuration)
|
|
||||||
Write-Host "监控状态 - 已中断: $([math]::Round($failureDuration))/${MonitorWindowSeconds}秒,剩余: ${remainingTime}秒" -ForegroundColor Yellow
|
|
||||||
|
|
||||||
if ($failureDuration -ge $MonitorWindowSeconds) {
|
|
||||||
# 持续失败时间超过监控窗口,开始关机倒计时
|
|
||||||
Write-Log -Level "CRITICAL" -Message "网络持续中断已超过 ${MonitorWindowSeconds} 秒,开始关机流程。"
|
|
||||||
Write-Host "========================================" -ForegroundColor Yellow
|
|
||||||
$shutdownCancelled = $false
|
|
||||||
$countdownEndTime = (Get-Date).AddSeconds($ShutdownCountdown)
|
|
||||||
while ((Get-Date) -lt $countdownEndTime -and -not $shutdownCancelled) {
|
|
||||||
$remainingSeconds = [math]::Ceiling(($countdownEndTime - (Get-Date)).TotalSeconds)
|
|
||||||
if ($remainingSeconds -le 0) { break }
|
|
||||||
|
|
||||||
Write-Log -Level "WARN" -Message "距离关机还有 $remainingSeconds 秒... 正在快速检测网络。"
|
|
||||||
Write-Host "----------------------------------------" -ForegroundColor Yellow
|
|
||||||
Write-Host "距离关机还有 $remainingSeconds 秒..." -ForegroundColor Yellow
|
|
||||||
Write-Host "正在快速检测网络连接... 按任意键可手动取消关机" -ForegroundColor Yellow # 在倒计时中再次检测网络
|
|
||||||
try {
|
|
||||||
if (Test-Connection -ComputerName $TargetIP -Count 1 -Quiet -ErrorAction SilentlyContinue) {
|
|
||||||
Write-Log -Level "SUCCESS" -Message "网络在倒计时期间恢复!取消关机。"
|
|
||||||
Write-Host "========================================" -ForegroundColor Green
|
|
||||||
Write-Host "[OK] 网络连接已恢复!取消关机操作" -ForegroundColor Green
|
|
||||||
Write-Host "========================================" -ForegroundColor Green
|
|
||||||
$failureStartTime = $null
|
|
||||||
$shutdownCancelled = $true
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
Write-Host "[X] 网络仍然中断" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "WARN" -Message "倒计时期间网络测试出现异常: $($_.Exception.Message)"
|
|
||||||
Write-Host "[X] 网络测试异常" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
# 检查是否有按键输入(手动取消)
|
|
||||||
$timeout = $CountdownPingInterval
|
|
||||||
$startTime = Get-Date
|
|
||||||
while (((Get-Date) - $startTime).TotalSeconds -lt $timeout -and -not $shutdownCancelled) {
|
|
||||||
if ([Console]::KeyAvailable) {
|
|
||||||
$null = [Console]::ReadKey($true) # 读取按键但不使用
|
|
||||||
Write-Log -Level "WARN" -Message "用户手动取消了关机操作" Write-Host ""
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
Write-Host "[INFO] 用户已手动取消关机,返回监控模式" -ForegroundColor Cyan
|
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
|
||||||
$failureStartTime = $null
|
|
||||||
$shutdownCancelled = $true
|
|
||||||
break }
|
|
||||||
Start-Sleep -Milliseconds 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $shutdownCancelled) {
|
|
||||||
Write-Log -Level "CRITICAL" -Message "关机倒计时完成,执行系统关机命令。"
|
|
||||||
try {
|
|
||||||
# 取消注释下一行以启用实际关机
|
|
||||||
# Stop-Computer -Force
|
|
||||||
Write-Host "模拟关机:Stop-Computer -Force (为防止意外,此行已注释)" -ForegroundColor Red
|
|
||||||
Write-Log -Level "CRITICAL" -Message "关机命令已执行(模拟模式)。"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Log -Level "CRITICAL" -Message "关机命令执行失败: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
# 脚本执行完关机命令后可以退出
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# 在监控窗口期内,继续等待
|
|
||||||
Write-Log -Level "INFO" -Message "等待 ${NormalPingInterval}秒后继续..."
|
|
||||||
Start-Sleep -Seconds $NormalPingInterval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#Requires -RunAsAdministrator
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
智能网络监控脚本 - 系统卸载脚本
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
完全卸载系统级部署的智能网络监控脚本
|
|
||||||
#>
|
|
||||||
|
|
||||||
$ProgramPath = "C:\Program Files\SmartNetworkMonitor"
|
|
||||||
$DataPath = "C:\ProgramData\SmartNetworkMonitor"
|
|
||||||
|
|
||||||
Write-Host "==========================================" -ForegroundColor Red
|
|
||||||
Write-Host "智能网络监控脚本 - 系统卸载" -ForegroundColor Red
|
|
||||||
Write-Host "==========================================" -ForegroundColor Red
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
Write-Host "将要删除:" -ForegroundColor Yellow
|
|
||||||
Write-Host "- 程序目录: $ProgramPath" -ForegroundColor White
|
|
||||||
Write-Host "- 数据目录: $DataPath (包含日志文件)" -ForegroundColor White
|
|
||||||
Write-Host "- 任务计划程序条目" -ForegroundColor White
|
|
||||||
Write-Host "- 事件日志源" -ForegroundColor White
|
|
||||||
Write-Host ""
|
|
||||||
|
|
||||||
$confirmation = Read-Host "确定要继续吗?这将删除所有相关文件和配置 (y/N)"
|
|
||||||
|
|
||||||
if ($confirmation -ne 'y' -and $confirmation -ne 'Y') {
|
|
||||||
Write-Host "卸载已取消" -ForegroundColor Green
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "正在卸载..." -ForegroundColor Yellow
|
|
||||||
|
|
||||||
# 停止并删除任务计划程序
|
|
||||||
try {
|
|
||||||
$task = Get-ScheduledTask -TaskName "Smart Network Shutdown Monitor" -ErrorAction SilentlyContinue
|
|
||||||
if ($task) {
|
|
||||||
Stop-ScheduledTask -TaskName "Smart Network Shutdown Monitor" -ErrorAction SilentlyContinue
|
|
||||||
Unregister-ScheduledTask -TaskName "Smart Network Shutdown Monitor" -Confirm:$false
|
|
||||||
Write-Host "[OK] 任务计划程序已删除" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[X] 删除任务计划程序失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# 删除事件日志源
|
|
||||||
try {
|
|
||||||
if ([System.Diagnostics.EventLog]::SourceExists("SmartNetworkMonitor")) {
|
|
||||||
[System.Diagnostics.EventLog]::DeleteEventSource("SmartNetworkMonitor")
|
|
||||||
Write-Host "[OK] 事件日志源已删除" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[X] 删除事件日志源失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# 删除程序目录
|
|
||||||
try {
|
|
||||||
if (Test-Path $ProgramPath) {
|
|
||||||
Remove-Item -Path $ProgramPath -Recurse -Force
|
|
||||||
Write-Host "[OK] 程序目录已删除: $ProgramPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[X] 删除程序目录失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
# 询问是否删除数据目录(包含日志)
|
|
||||||
Write-Host ""
|
|
||||||
$deleteData = Read-Host "是否同时删除数据目录和所有日志文件?(y/N)"
|
|
||||||
|
|
||||||
if ($deleteData -eq 'y' -or $deleteData -eq 'Y') {
|
|
||||||
try {
|
|
||||||
if (Test-Path $DataPath) {
|
|
||||||
Remove-Item -Path $DataPath -Recurse -Force
|
|
||||||
Write-Host "[OK] 数据目录已删除: $DataPath" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[X] 删除数据目录失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Host "[INFO] 数据目录保留: $DataPath" -ForegroundColor Cyan
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "[OK] 卸载完成!" -ForegroundColor Green
|
|
||||||
Read-Host "按任意键退出"
|
|
||||||
Reference in New Issue
Block a user