feat: add interactive config setup on first start, install scripts, and uninstall cleanup

This commit is contained in:
2026-03-24 22:46:45 +08:00
parent 35bf24f0f3
commit 850cc49daf
6 changed files with 257 additions and 13 deletions

View File

@@ -2,28 +2,37 @@
持续监测目标网络连通性并在网络断开超时后自动关闭系统的跨平台常驻服务程序。
> **注意:** 本程序涉及系统服务注册与网络监控,所有安装、卸载及服务管理操作均**必须在管理员 (Windows) 或 root (Linux) 权限**下执行。
## 安装指南 (Installation)
本程序支持安装为操作系统的后台服务,并自动注册全局环境变量以便统一管理。
### Windows 系统
推荐以拥有管理员权限的系统服务形式部署。
**以管理员身份打开 PowerShell**,执行以下一键安装命令:
1. **获取程序**
前往 [Releases](https://github.com/UnbalancedCat/smart-shutdown/releases) 页面,下载最新的 `smart-shutdown_windows_amd64.exe` (或其它架构版本)。
```powershell
irm https://raw.githubusercontent.com/UnbalancedCat/smart-shutdown/main/install.ps1 | iex
```
2. **终端安装**
在下载目录的空白处右键选择 **以管理员身份打开 PowerShell 或 CMD**,执行如下指令:
```powershell
.\smart-shutdown_windows_amd64.exe install
smart-shutdown start
```
> **说明:** `install` 指令会自动将程序复制到 `C:\Program Files\SmartShutdown\` 并写入系统 `PATH` 环境变量中,同时注册为 Windows 开机自启服务。此后您可在任意终端直接使用 `smart-shutdown` 命令。
> **说明:** 该命令会自动下载最新版本,执行 `install` 将程序复制到 `C:\Program Files\SmartShutdown\` 并写入系统 `PATH` 环境变量,同时注册为 Windows 开机自启服务。此后您可在任意管理员终端直接使用 `smart-shutdown` 命令。
如需手动安装,也可前往 [Releases](https://github.com/UnbalancedCat/smart-shutdown/releases) 页面下载对应架构版本,然后在管理员终端中执行:
```powershell
.\smart-shutdown_windows_amd64.exe install
smart-shutdown start
```
### Linux (Ubuntu/Debian/CentOS 等)
如您具备 `sudo` 权限,可在终端中执行以下单行命令进行自动化下载与 `systemd` 服务部署
在终端中执行以下一键安装命令
```bash
curl -sSL https://raw.githubusercontent.com/UnbalancedCat/smart-shutdown/main/install.sh | sudo sh
```
如需手动安装,可执行以下命令:
```bash
sudo curl -sSL https://github.com/UnbalancedCat/smart-shutdown/releases/latest/download/smart-shutdown_linux_amd64 -o /usr/local/bin/smart-shutdown && \
@@ -32,6 +41,41 @@ sudo smart-shutdown install && \
sudo smart-shutdown start
```
## 快速开始
安装完成后,在管理员 / root 终端中执行首次启动:
```bash
smart-shutdown start
```
如果程序未检测到配置文件,将自动引导您完成初始设置:
```
========== 首次配置 ==========
未检测到配置文件,将引导您完成初始设置。直接按回车使用 [默认值]。
目标监控 IP 地址 [192.168.3.1]: <输入您的目标 IP 或直接回车>
断网容忍超时时长 (秒) [180]:
关机倒计时缓冲 (秒) [60]:
探测发包间隔 (秒) [15]:
配置已保存至: C:\ProgramData\SmartNetworkMonitor\config.json
```
> **提示:** 服务注册后将**随系统开机自动启动**,无需手动干预。如需暂停监控,请执行 `smart-shutdown stop`。
## Windows 注意事项
如您在 Windows 上使用 `sudo` 命令Windows 11 24H2+ 内置)来执行本程序,请注意:
Windows `sudo` 默认运行在 **ForceNewWindow强制新窗口** 模式下,提权进程的输出将打印到一个瞬间关闭的新窗口中,**导致 `status` 等展示类命令的输出不可见。**
**解决方法(任选其一):**
1. **切换 sudo 为 Inline 模式**:打开 **系统设置 → 开发者选项 → 启用 sudo**,将模式改为 **内联 (Inline)**。此后 `sudo smart-shutdown status` 的输出将正常回显至当前终端。
2. **直接使用管理员终端**:右键点击终端图标,选择 **以管理员身份运行**,随后无需 `sudo` 前缀即可执行所有指令。
## CLI 终端管理指令 (Commands)
完成服务注册后,可通过以下指令直接管理守护进程状态流。
@@ -80,7 +124,7 @@ smart-shutdown uninstall
| 参数项 | 含义与功能说明 | 默认值 |
|:---|:---|:---|
| `TargetIP` | 需发送 ICMP 包验证联通性的目标 IPv4 地址。 | `192.168.3.3` |
| `TargetIP` | 需发送 ICMP 包验证联通性的目标 IPv4 地址。 | `192.168.3.1` |
| `MonitorWindowSeconds` | 网络中断被判定为异常并触发系统关机前,所能容忍的最长超时时长 (秒)。 | `180` |
| `ShutdownCountdown` | 容忍超限后,执行正式关机系统指令的警告倒计时缓冲时间 (秒)。 | `60` |
| `NormalPingInterval` | 网络连通性正常时,每次静默发包探测的间隔时间 (秒)。 | `15` |

View File

@@ -130,6 +130,12 @@ func main() {
}
}
}
if config.ConfirmConfigCleanup() {
configDir := config.GetConfigDir()
os.RemoveAll(configDir)
fmt.Println("配置文件与日志已清除。")
}
},
},
{
@@ -178,6 +184,14 @@ func main() {
Use: "start",
Short: "唤起执行守护后台",
Run: func(cmd *cobra.Command, args []string) {
if !config.ConfigFileExists() {
newCfg, err := config.InteractiveSetup()
if err != nil {
logger.Fail("配置初始化失败: %v", err)
return
}
cfg = newCfg
}
handleServiceControl(svc, "start")
},
},

27
install.ps1 Normal file
View File

@@ -0,0 +1,27 @@
# Smart Network Shutdown Monitor - Windows Installer
# Usage: irm https://raw.githubusercontent.com/UnbalancedCat/smart-shutdown/main/install.ps1 | iex
$ErrorActionPreference = "Stop"
# Check admin privileges
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host "[FAIL] 此安装脚本必须以管理员身份运行。请右键 PowerShell 选择「以管理员身份运行」后重试。" -ForegroundColor Red
exit 1
}
$arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" }
$fileName = "smart-shutdown_windows_${arch}.exe"
$downloadUrl = "https://github.com/UnbalancedCat/smart-shutdown/releases/latest/download/$fileName"
$tempPath = Join-Path $env:TEMP "smart-shutdown.exe"
Write-Host "正在下载最新版本: $fileName ..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempPath -UseBasicParsing
Write-Host "正在执行安装与服务注册..." -ForegroundColor Cyan
& $tempPath install
Write-Host "正在启动后台服务..." -ForegroundColor Cyan
smart-shutdown start
Remove-Item $tempPath -Force -ErrorAction SilentlyContinue
Write-Host "安装完成。此后可在管理员终端中直接使用 smart-shutdown 命令。" -ForegroundColor Green

35
install.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/sh
# Smart Network Shutdown Monitor - Linux Installer
# Usage: curl -sSL https://raw.githubusercontent.com/UnbalancedCat/smart-shutdown/main/install.sh | sudo sh
set -e
# Check root privileges
if [ "$(id -u)" -ne 0 ]; then
echo "[FAIL] 此安装脚本必须以 root 权限运行。请使用 sudo 执行。"
exit 1
fi
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
armv7l) ARCH="armv7" ;;
*) echo "[FAIL] 不支持的架构: $ARCH"; exit 1 ;;
esac
FILE_NAME="smart-shutdown_linux_${ARCH}"
DOWNLOAD_URL="https://github.com/UnbalancedCat/smart-shutdown/releases/latest/download/$FILE_NAME"
TARGET_PATH="/usr/local/bin/smart-shutdown"
echo "正在下载最新版本: $FILE_NAME ..."
curl -sSL "$DOWNLOAD_URL" -o "$TARGET_PATH"
chmod +x "$TARGET_PATH"
echo "正在执行安装与服务注册..."
smart-shutdown install
echo "正在启动后台服务..."
smart-shutdown start
echo "安装完成。此后可直接使用 smart-shutdown 命令。"

View File

@@ -19,7 +19,7 @@ type Config struct {
func DefaultConfig() *Config {
return &Config{
TargetIP: "192.168.3.3",
TargetIP: "192.168.3.1",
MonitorWindowSeconds: 180,
ShutdownCountdown: 60,
NormalPingInterval: 15,

124
pkg/config/interactive.go Normal file
View File

@@ -0,0 +1,124 @@
package config
import (
"bufio"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"strings"
)
// ConfigFileExists checks if the config.json file exists at the expected location.
func ConfigFileExists() bool {
configPath := filepath.Join(GetConfigDir(), "config.json")
_, err := os.Stat(configPath)
return err == nil
}
// isTerminal checks if stdin is connected to a terminal (not a pipe).
func isTerminal() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return fi.Mode()&os.ModeCharDevice != 0
}
// InteractiveSetup prompts the user to configure each parameter interactively.
// If stdin is not a terminal (pipe mode), it silently saves and returns defaults.
func InteractiveSetup() (*Config, error) {
cfg := DefaultConfig()
if !isTerminal() {
if err := SaveConfig(cfg); err != nil {
return nil, fmt.Errorf("写入默认配置失败: %v", err)
}
return cfg, nil
}
fmt.Println("\n========== 首次配置 ==========")
fmt.Println("未检测到配置文件,将引导您完成初始设置。直接按回车使用 [默认值]。")
scanner := bufio.NewScanner(os.Stdin)
cfg.TargetIP = promptString(scanner, "目标监控 IP 地址", cfg.TargetIP, func(s string) error {
if net.ParseIP(s) == nil {
return fmt.Errorf("非法 IP 地址: %s", s)
}
return nil
})
cfg.MonitorWindowSeconds = promptInt(scanner, "断网容忍超时时长 (秒)", cfg.MonitorWindowSeconds)
cfg.ShutdownCountdown = promptInt(scanner, "关机倒计时缓冲 (秒)", cfg.ShutdownCountdown)
cfg.NormalPingInterval = promptInt(scanner, "探测发包间隔 (秒)", cfg.NormalPingInterval)
if err := SaveConfig(cfg); err != nil {
return nil, fmt.Errorf("写入配置文件失败: %v", err)
}
configPath := filepath.Join(GetConfigDir(), "config.json")
fmt.Printf("\n配置已保存至: %s\n", configPath)
return cfg, nil
}
// ConfirmConfigCleanup prompts the user to confirm deletion of config and log files.
// Returns true if the user confirms. Non-terminal stdin defaults to no.
func ConfirmConfigCleanup() bool {
if !isTerminal() {
return false
}
configDir := GetConfigDir()
configPath := filepath.Join(configDir, "config.json")
if _, err := os.Stat(configPath); err != nil {
return false
}
fmt.Printf("检测到配置文件: %s\n是否一并清除配置文件与日志[y/N]: ", configDir)
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
return strings.EqualFold(strings.TrimSpace(scanner.Text()), "y")
}
return false
}
func promptString(scanner *bufio.Scanner, label, defaultVal string, validate func(string) error) string {
for {
fmt.Printf("\n%s [%s]: ", label, defaultVal)
if !scanner.Scan() {
return defaultVal
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
return defaultVal
}
if validate != nil {
if err := validate(input); err != nil {
fmt.Printf(" 输入无效: %v请重新输入。\n", err)
continue
}
}
return input
}
}
func promptInt(scanner *bufio.Scanner, label string, defaultVal int) int {
for {
fmt.Printf("%s [%d]: ", label, defaultVal)
if !scanner.Scan() {
return defaultVal
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
return defaultVal
}
val, err := strconv.Atoi(input)
if err != nil || val <= 0 {
fmt.Println(" 输入无效: 必须为正整数,请重新输入。")
continue
}
return val
}
}