refactor: 使用行业标准方案重构服务重启逻辑

重构内容:
- 移除复杂的 sudo systemctl restart 方案
- 改用 os.Exit(0) + systemd Restart=always 的标准做法
- 删除 sudoers 配置及相关代码
- 删除 sub2api-sudoers 文件

优势:
- 代码从 85+ 行简化到 47 行
- 无需 sudo 权限配置
- 无需特殊用户 shell 配置
- 更简单、更可靠
- 符合行业最佳实践(Docker/K8s 等均采用此方案)

工作原理:
- 服务调用 os.Exit(0) 优雅退出
- systemd 检测到退出后自动重启(Restart=always)
This commit is contained in:
shaw
2025-12-18 20:32:24 +08:00
parent f0e89992f7
commit 8e81e395b3
3 changed files with 18 additions and 121 deletions

View File

@@ -1,88 +1,39 @@
package sysutil
import (
"fmt"
"log"
"os"
"os/exec"
"runtime"
"time"
)
const serviceName = "sub2api"
// findExecutable finds the full path of an executable
// by checking common system paths
func findExecutable(name string) string {
// First try exec.LookPath (uses current PATH)
if path, err := exec.LookPath(name); err == nil {
return path
}
// Fallback: check common paths
commonPaths := []string{
"/usr/bin/" + name,
"/bin/" + name,
"/usr/sbin/" + name,
"/sbin/" + name,
}
for _, path := range commonPaths {
if _, err := os.Stat(path); err == nil {
return path
}
}
// Return the name as-is and let exec fail with a clear error
return name
}
// RestartService triggers a service restart via systemd.
// RestartService triggers a service restart by gracefully exiting.
//
// IMPORTANT: This function initiates the restart and returns immediately.
// The actual restart happens asynchronously - the current process will be killed
// by systemd and a new process will be started.
//
// We use Start() instead of Run() because:
// - systemctl restart will kill the current process first
// - Run() waits for completion, but the process dies before completion
// - Start() spawns the command independently, allowing systemd to handle the full cycle
// This relies on systemd's Restart=always configuration to automatically
// restart the service after it exits. This is the industry-standard approach:
// - Simple and reliable
// - No sudo permissions needed
// - No complex process management
// - Leverages systemd's native restart capability
//
// Prerequisites:
// - Linux OS with systemd
// - NOPASSWD sudo access configured (install.sh creates /etc/sudoers.d/sub2api)
// - Service configured with Restart=always in systemd unit file
func RestartService() error {
if runtime.GOOS != "linux" {
return fmt.Errorf("systemd restart only available on Linux")
log.Println("Service restart via exit only works on Linux with systemd")
return nil
}
log.Println("Initiating service restart...")
log.Println("Initiating service restart by graceful exit...")
log.Println("systemd will automatically restart the service (Restart=always)")
// Find full paths for sudo and systemctl
// This ensures the commands work even if PATH is limited in systemd service
sudoPath := findExecutable("sudo")
systemctlPath := findExecutable("systemctl")
// Give a moment for logs to flush and response to be sent
go func() {
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
log.Printf("Using sudo: %s, systemctl: %s", sudoPath, systemctlPath)
// The sub2api user has NOPASSWD sudo access for systemctl commands
// (configured by install.sh in /etc/sudoers.d/sub2api).
// Use -n (non-interactive) to prevent sudo from waiting for password input
//
// Use setsid to create a new session, ensuring the child process
// survives even if the parent process is killed by systemctl restart
setsidPath := findExecutable("setsid")
cmd := exec.Command(setsidPath, sudoPath, "-n", systemctlPath, "restart", serviceName)
// Detach from parent's stdio to ensure clean separation
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to initiate service restart: %w", err)
}
log.Println("Service restart initiated successfully")
return nil
}