mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-13 17:07:34 +08:00
* 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>
466 lines
12 KiB
Go
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
|
|
}
|