feat: add interactive config setup on first start, install scripts, and uninstall cleanup
This commit is contained in:
68
README.md
68
README.md
@@ -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` |
|
||||
|
||||
@@ -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
27
install.ps1
Normal 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
35
install.sh
Normal 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 命令。"
|
||||
@@ -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
124
pkg/config/interactive.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user