mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-14 02:24:44 +08:00
455 lines
12 KiB
Go
455 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 = 60 * 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 {
|
|
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 buf.Bytes(), &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, "is-enabled", name)
|
|
if cmdErr == nil || strings.Contains(string(out), "disabled") {
|
|
return true, nil
|
|
}
|
|
return false, 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", "status":
|
|
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
|
|
}
|
|
return strings.TrimSpace(output) != "", nil
|
|
case "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)
|
|
}
|
|
|
|
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.*$`),
|
|
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
|
|
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
|
|
}}
|
|
}
|
|
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) {
|
|
_, 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 *openrcManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
|
|
cmdArgs := m.buildBaseCommand()
|
|
service := config.ServiceName[m.name]
|
|
switch action {
|
|
case "is-enabled":
|
|
return []string{
|
|
"sh",
|
|
"-c",
|
|
fmt.Sprintf("if ls /etc/runlevels/default/%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
|
|
case "is-active", "status":
|
|
return []string{
|
|
"sh",
|
|
"-c",
|
|
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
|
|
}, nil
|
|
case "enable":
|
|
return []string{"rc-update", "add", service, "default"}, nil
|
|
case "disable":
|
|
return []string{"rc-update", "del", service, "default"}, nil
|
|
default:
|
|
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) {
|
|
files, err := os.ReadDir("/etc/init.d/")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
|
|
}
|
|
keyword = strings.ToLower(keyword)
|
|
var services []string
|
|
for _, file := range files {
|
|
if strings.Contains(file.Name(), keyword) {
|
|
services = append(services, file.Name())
|
|
}
|
|
}
|
|
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 }
|
|
|
|
func ReinitializeManager() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
globalManager = nil
|
|
return InitializeGlobalManager()
|
|
}
|
|
|
|
func SetManagerPriority(order []string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
managerPriority = order
|
|
}
|