1Panel/backend/utils/systemctl/managers.go
巴山夜语 79020abb1c
feat(systemctl): implement service manager initialization and command execution (#8380)
* feat(systemctl): 实现服务管理器初始化和命令执行

- 新增 systemctl 包,实现对 systemd、openrc 和 sysvinit 三种服务管理器的支持
- 添加服务状态检查、启动、停止、重启和启用/禁用功能
- 实现服务发现和智能服务名处理
- 添加配置文件查看功能
- 优化错误处理和日志记录

* refactor(system): 重构系统服务管理逻辑

- 引入 systemctl 工具包以统一处理系统服务
- 优化服务状态获取、配置文件路径解析等逻辑
- 重构 HostToolService 中的 GetToolStatus 方法
- 更新 DockerService、SettingService 等相关服务的处理方式
- 调整快照创建和恢复过程中的服务处理逻辑

* feat(utils): 添加目录复制功能并优化文件复制逻辑

- 新增 CopyDirs 函数,用于复制整个目录及其内容
- 添加对符号链接的复制支持
- 实现通用的 Copy 函数,根据文件类型自动选择 CopyFile 或 CopyDirs
- 在 CopyFile 函数中增加对源文件是目录的检查和错误提示

* refactortoolbox: 重构 Fail2ban 和 Pure-FTPd 的管理逻辑

- 优化了 Fail2ban 和 Pure-FTPd 的启动、停止、重启等操作的实现
- 改进了 Fail2ban 版本信息的获取方法
- 统一了错误处理和日志记录的格式
- 调整了部分导入的包,提高了代码的可维护性

* build: 禁用 CGO 以提高构建性能和兼容性

- 在 Linux 后端构建命令中添加 CGO_ENABLED=0 环境变量
- 此修改可以提高构建速度,并确保生成的二进制文件在没有 C 库依赖的环境中也能运行

* refactor(docker): 重构 Docker 服务的重启和操作逻辑

- 添加 isDockerSnapInstalled 函数来判断 Docker 是否通过 Snap 安装
- 在 OperateDocker 和 restartDocker 函数中增加对 Snap 安装的处理
- 移除未使用的 getDockerRestartCommand 函数

* fix(service): 优化快照恢复后的服务重启逻辑

- 在使用 systemd 管理服务时,增加 daemon-reload 操作以确保服务配置更新
- 重启 1panel 服务,以应用快照恢复的更改

* refactor(server): 支持非 systemd 系统的恢复操作

- 增加 isSystemd 函数判断系统是否为 systemd 类型
- 根据系统类型选择性地恢复服务文件
- 兼容 systemd 和非 systemd 系统的恢复流程

* fix(upgrade): 优化升级过程中的服务重启逻辑

- 移动服务重启逻辑到版本号更新之后,修复因提前重启导致的版本号未更新BUG。
- 在 systemctl 重启之前添加 daemon-reload 命令

---------

Co-authored-by: gcsong023 <gcsong023@users.noreply.github.com>
2025-04-17 10:26:13 +08:00

466 lines
12 KiB
Go

package systemctl
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
)
var (
managers = make(map[string]ServiceManager)
mu sync.RWMutex
globalManager ServiceManager
managerPriority = []string{"systemd", "openrc", "sysvinit"}
)
const (
defaultCommandTimeout = 30 * time.Second
serviceCheckTimeout = 5 * time.Second
)
type ServiceManager interface {
Name() string
IsAvailable() bool
ServiceExists(*ServiceConfig) (bool, error)
BuildCommand(string, *ServiceConfig) ([]string, error)
ParseStatus(string, *ServiceConfig, string) (bool, error)
FindServices(string) ([]string, error)
}
type baseManager struct {
name string
cmdTool string
activeRegex *regexp.Regexp
enabledRegex *regexp.Regexp
}
func (b *baseManager) Name() string { return b.name }
func isRootUser() bool {
return os.Geteuid() == 0
}
func (b *baseManager) buildBaseCommand() []string {
var cmdArgs []string
if !isRootUser() {
cmdArgs = append(cmdArgs, "sudo")
}
cmdArgs = append(cmdArgs, b.cmdTool)
return cmdArgs
}
func (b *baseManager) commonServiceExists(config *ServiceConfig, checkFn func(string) (bool, error)) (bool, error) {
if name := config.ServiceName[b.name]; name != "" {
exists, checkErr := checkFn(name)
if checkErr != nil {
// global.LOG.Warnf("Service existence check failed %s: %v", b.name, checkErr)
return false, nil
}
return exists, nil
}
return false, nil
}
func (b *baseManager) ParseStatus(output string, _ *ServiceConfig, statusType string) (bool, error) {
if output == "" {
return false, nil
}
switch statusType {
case "active":
if b.activeRegex == nil {
return false, nil
}
return b.activeRegex.MatchString(output), nil
case "enabled":
if b.enabledRegex == nil {
return false, nil
}
return b.enabledRegex.MatchString(output), nil
default:
return false, nil
}
}
func registerManager(m ServiceManager) {
mu.Lock()
defer mu.Unlock()
managers[m.Name()] = m
}
func init() {
for _, mgr := range []ServiceManager{
newSystemdManager(),
newOpenrcManager(),
newSysvinitManager(),
} {
registerManager(mgr)
}
}
func InitializeGlobalManager() (err error) {
for _, name := range managerPriority {
if mgr, ok := managers[name]; ok && mgr.IsAvailable() {
if testManager(mgr) {
globalManager = mgr
global.LOG.Infof("Initialized service manager: %s", name)
return
}
}
}
err = fmt.Errorf("no available service manager found (tried: %v)", managerPriority)
return
}
func testManager(mgr ServiceManager) bool {
_, err := mgr.BuildCommand("status", &ServiceConfig{
ServiceName: map[string]string{mgr.Name(): "test-service"},
})
return err == nil
}
func GetGlobalManager() ServiceManager {
if globalManager == nil {
mu.Lock()
defer mu.Unlock()
if globalManager == nil {
return initializeWithRetry()
}
}
return globalManager
}
func initializeWithRetry() ServiceManager {
const (
maxRetries = 5
initialWait = 1 * time.Second
)
backoff := initialWait
for attempt := 1; attempt <= maxRetries; attempt++ {
if err := InitializeGlobalManager(); err == nil {
return globalManager
}
logMessage := fmt.Sprintf("Manager init attempt %d/%d failed", attempt, maxRetries)
if global.LOG != nil {
global.LOG.Warn(logMessage)
} else {
fmt.Printf("[WARN] %s\n", logMessage)
}
if attempt < maxRetries {
time.Sleep(backoff)
backoff *= 2
}
}
if global.LOG != nil {
global.LOG.Error("All manager initialization attempts failed")
} else {
fmt.Println("[FATAL] All manager initialization attempts failed")
}
panic("unable to initialize service manager")
}
func executeCommand(ctx context.Context, command string, args ...string) ([]byte, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, defaultCommandTimeout)
defer cancel()
}
cmd := exec.CommandContext(ctx, command, args...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
return nil, &CommandError{
Cmd: cmd.String(),
Output: buf.String(),
Err: err,
}
}
return buf.Bytes(), nil
}
type systemdManager struct{ baseManager }
func newSystemdManager() ServiceManager {
return &systemdManager{baseManager{
name: "systemd",
cmdTool: "systemctl",
activeRegex: regexp.MustCompile(`(?i)Active:\s+active\b`),
enabledRegex: regexp.MustCompile(`(?i)^\s*enabled\s*$`),
}}
}
func (m *systemdManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *systemdManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), serviceCheckTimeout)
defer cancel()
out, cmdErr := executeCommand(ctx, m.cmdTool, "list-unit-files", name)
if cmdErr != nil {
return false, fmt.Errorf("systemctl list-unit-files failed: %w", cmdErr)
}
return bytes.Contains(out, []byte(name)), nil
})
}
func (m *systemdManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
cmdArgs = append(cmdArgs, "is-enabled", service)
default:
cmdArgs = append(cmdArgs, action, service)
}
return cmdArgs, nil
}
func (m *systemdManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "could not be found") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *systemdManager) FindServices(keyword string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out, err := executeCommand(ctx, m.cmdTool, "list-unit-files", "--type=service", "--no-legend")
if err != nil {
return nil, fmt.Errorf("failed to list systemd services: %w", err)
}
var services []string
lines := strings.Split(string(out), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
serviceName := fields[0]
if strings.Contains(serviceName, keyword) {
services = append(services, serviceName)
}
}
return services, nil
}
type sysvinitManager struct{ baseManager }
func newSysvinitManager() ServiceManager {
return &sysvinitManager{baseManager{
name: "sysvinit",
cmdTool: "service",
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
}}
}
func (m *sysvinitManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *sysvinitManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
_, err := os.Stat(filepath.Join("/etc/init.d", name))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
}
return true, nil
})
}
func (m *sysvinitManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
return []string{
"sh",
"-c",
fmt.Sprintf("if ls /etc/rc*.d/S*%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
case "is-active":
return []string{
"sh",
"-c",
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
}, nil
default:
cmdArgs := m.buildBaseCommand()
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
}
func (m *sysvinitManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
serviceName := config.ServiceName[m.name]
switch statusType {
case "enabled":
if strings.Contains(output, "no such file or directory") {
return false, nil
}
// 关键逻辑:如果 find 命令有输出(找到符号链接),则服务已启用
return strings.TrimSpace(output) != "", nil
case "active":
// 关键逻辑:如果输出包含 "running" 或 "active",则服务处于活动状态
if strings.Contains(output, "not found") {
return false, nil
}
if strings.Contains(output, "running") || strings.Contains(output, "active") {
return true, nil
}
default:
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
global.LOG.Debugf("[sysvinit] Status parse failed. [ServiceName:%s] Type: %s, Output: %q", serviceName, statusType, output)
}
return result, err
}
return false, fmt.Errorf("unsupported status type: %s", statusType)
// service := config.ServiceName[m.name]
// serviceRegex := regexp.MustCompile(fmt.Sprintf(`\b%s\b`, regexp.QuoteMeta(service)))
// lines := strings.Split(output, "\n")
// for _, line := range lines {
// if serviceRegex.MatchString(line) {
// if strings.Contains(line, "not found") {
// return false, nil
// }
// result, err := m.baseManager.ParseStatus(line, config, statusType)
// if err != nil {
// return false, err
// }
// return result, nil
// }
// }
// return false, nil
}
func (m *sysvinitManager) FindServices(keyword string) ([]string, error) {
files, err := os.ReadDir("/etc/init.d/")
if err != nil {
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
}
var services []string
for _, file := range files {
if strings.Contains(file.Name(), keyword) {
services = append(services, file.Name())
}
}
return services, nil
}
type openrcManager struct{ baseManager }
func newOpenrcManager() ServiceManager {
return &openrcManager{baseManager{
name: "openrc",
cmdTool: "rc-service",
activeRegex: regexp.MustCompile(`(?i)^\s*status:\s+(started|running|active)\s*$`),
enabledRegex: regexp.MustCompile(`(?i)^[^\|]+\|\s*(default|enabled)\b.*$`),
}}
}
func (m *openrcManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *openrcManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), serviceCheckTimeout)
defer cancel()
out, err := executeCommand(ctx, m.cmdTool, "-l")
if err != nil {
return false, fmt.Errorf("rc-service -l failed: %w", err)
}
return bytes.Contains(out, []byte(name)), nil
})
}
func (m *openrcManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
if action == "is-enabled" {
cmdArgs = []string{"rc-update", "check", service}
return cmdArgs, nil
}
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
func (m *openrcManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "does not exist") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *openrcManager) FindServices(keyword string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out, err := executeCommand(ctx, m.cmdTool, "-l")
if err != nil {
return nil, fmt.Errorf("failed to list openrc services: %w", err)
}
var services []string
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.Contains(line, keyword) {
services = append(services, strings.TrimSpace(line))
}
}
return services, nil
}
type CommandError struct {
Cmd string
Err error
Output string
}
func (e CommandError) Error() string {
return fmt.Sprintf("command %q failed: %v \nOutput: %s",
e.Cmd, e.Err, e.Output)
}
func (e CommandError) Unwrap() error { return e.Err }
// ReinitializeManager for test
func ReinitializeManager() error {
mu.Lock()
defer mu.Unlock()
// initOnce = sync.Once{}
globalManager = nil
return InitializeGlobalManager()
}
// SetManagerPriority for test
func SetManagerPriority(order []string) {
mu.Lock()
defer mu.Unlock()
managerPriority = order
}