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:
@@ -1,88 +1,39 @@
|
|||||||
package sysutil
|
package sysutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceName = "sub2api"
|
// RestartService triggers a service restart by gracefully exiting.
|
||||||
|
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// IMPORTANT: This function initiates the restart and returns immediately.
|
// This relies on systemd's Restart=always configuration to automatically
|
||||||
// The actual restart happens asynchronously - the current process will be killed
|
// restart the service after it exits. This is the industry-standard approach:
|
||||||
// by systemd and a new process will be started.
|
// - Simple and reliable
|
||||||
//
|
// - No sudo permissions needed
|
||||||
// We use Start() instead of Run() because:
|
// - No complex process management
|
||||||
// - systemctl restart will kill the current process first
|
// - Leverages systemd's native restart capability
|
||||||
// - Run() waits for completion, but the process dies before completion
|
|
||||||
// - Start() spawns the command independently, allowing systemd to handle the full cycle
|
|
||||||
//
|
//
|
||||||
// Prerequisites:
|
// Prerequisites:
|
||||||
// - Linux OS with systemd
|
// - 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 {
|
func RestartService() error {
|
||||||
if runtime.GOOS != "linux" {
|
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
|
// Give a moment for logs to flush and response to be sent
|
||||||
// This ensures the commands work even if PATH is limited in systemd service
|
go func() {
|
||||||
sudoPath := findExecutable("sudo")
|
time.Sleep(100 * time.Millisecond)
|
||||||
systemctlPath := findExecutable("systemctl")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ declare -A MSG_ZH=(
|
|||||||
["dirs_configured"]="目录配置完成"
|
["dirs_configured"]="目录配置完成"
|
||||||
["installing_service"]="正在安装 systemd 服务..."
|
["installing_service"]="正在安装 systemd 服务..."
|
||||||
["service_installed"]="systemd 服务已安装"
|
["service_installed"]="systemd 服务已安装"
|
||||||
["setting_up_sudoers"]="正在配置 sudoers..."
|
|
||||||
["sudoers_configured"]="sudoers 配置完成"
|
|
||||||
["sudoers_failed"]="sudoers 验证失败,已移除文件"
|
|
||||||
["ready_for_setup"]="准备就绪,可以启动设置向导"
|
["ready_for_setup"]="准备就绪,可以启动设置向导"
|
||||||
|
|
||||||
# Completion
|
# Completion
|
||||||
@@ -173,9 +170,6 @@ declare -A MSG_EN=(
|
|||||||
["dirs_configured"]="Directories configured"
|
["dirs_configured"]="Directories configured"
|
||||||
["installing_service"]="Installing systemd service..."
|
["installing_service"]="Installing systemd service..."
|
||||||
["service_installed"]="Systemd service installed"
|
["service_installed"]="Systemd service installed"
|
||||||
["setting_up_sudoers"]="Setting up sudoers..."
|
|
||||||
["sudoers_configured"]="Sudoers configured"
|
|
||||||
["sudoers_failed"]="Sudoers validation failed, removing file"
|
|
||||||
["ready_for_setup"]="Ready for Setup Wizard"
|
["ready_for_setup"]="Ready for Setup Wizard"
|
||||||
|
|
||||||
# Completion
|
# Completion
|
||||||
@@ -521,35 +515,6 @@ setup_directories() {
|
|||||||
print_success "$(msg 'dirs_configured')"
|
print_success "$(msg 'dirs_configured')"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Setup sudoers for service restart
|
|
||||||
setup_sudoers() {
|
|
||||||
print_info "$(msg 'setting_up_sudoers')"
|
|
||||||
|
|
||||||
# Always generate sudoers file from script (not from tar.gz)
|
|
||||||
# This ensures the latest configuration is used even with older releases
|
|
||||||
# Support both /bin/systemctl and /usr/bin/systemctl for different distros
|
|
||||||
cat > /etc/sudoers.d/sub2api << 'EOF'
|
|
||||||
# Sudoers configuration for Sub2API
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /bin/systemctl restart sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /bin/systemctl stop sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /bin/systemctl start sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /usr/bin/systemctl start sub2api
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Set correct permissions (required for sudoers files)
|
|
||||||
chmod 440 /etc/sudoers.d/sub2api
|
|
||||||
|
|
||||||
# Validate sudoers file
|
|
||||||
if visudo -c -f /etc/sudoers.d/sub2api &>/dev/null; then
|
|
||||||
print_success "$(msg 'sudoers_configured')"
|
|
||||||
else
|
|
||||||
print_warning "$(msg 'sudoers_failed')"
|
|
||||||
rm -f /etc/sudoers.d/sub2api
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Install systemd service
|
# Install systemd service
|
||||||
install_service() {
|
install_service() {
|
||||||
print_info "$(msg 'installing_service')"
|
print_info "$(msg 'installing_service')"
|
||||||
@@ -716,7 +681,6 @@ uninstall() {
|
|||||||
|
|
||||||
print_info "$(msg 'removing_files')"
|
print_info "$(msg 'removing_files')"
|
||||||
rm -f /etc/systemd/system/sub2api.service
|
rm -f /etc/systemd/system/sub2api.service
|
||||||
rm -f /etc/sudoers.d/sub2api
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|
||||||
print_info "$(msg 'removing_install_dir')"
|
print_info "$(msg 'removing_install_dir')"
|
||||||
@@ -787,7 +751,6 @@ main() {
|
|||||||
create_user
|
create_user
|
||||||
setup_directories
|
setup_directories
|
||||||
install_service
|
install_service
|
||||||
setup_sudoers
|
|
||||||
prepare_for_setup
|
prepare_for_setup
|
||||||
print_completion
|
print_completion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# Sudoers configuration for Sub2API
|
|
||||||
# This file allows the sub2api service user to restart the service without password
|
|
||||||
#
|
|
||||||
# Installation:
|
|
||||||
# sudo cp sub2api-sudoers /etc/sudoers.d/sub2api
|
|
||||||
# sudo chmod 440 /etc/sudoers.d/sub2api
|
|
||||||
#
|
|
||||||
# SECURITY NOTE: This grants limited sudo access only for service management
|
|
||||||
|
|
||||||
# Allow sub2api user to restart the service without password
|
|
||||||
# Support both /bin/systemctl (Debian/Ubuntu) and /usr/bin/systemctl (RHEL/CentOS)
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /bin/systemctl restart sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /bin/systemctl stop sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /bin/systemctl start sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop sub2api
|
|
||||||
sub2api ALL=(ALL) NOPASSWD: /usr/bin/systemctl start sub2api
|
|
||||||
Reference in New Issue
Block a user