1Panel/backend/utils/systemctl/configloade.go
2025-06-26 06:39:22 +00:00

393 lines
9.7 KiB
Go

package systemctl
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"go4.org/syncutil/singleflight"
"golang.org/x/sync/errgroup"
)
var (
aliasFile string
serviceAliases sync.Map
saveTimer *time.Timer
saveMutex sync.Mutex
afterSaveTime = 20 * time.Second
)
var (
ErrServiceNotFound = errors.New("service not found")
ErrDiscoveryTimeout = errors.New("service discovery timeout for: %w")
ErrServiceDiscovery = errors.New("service discovery failed for: %w")
ErrNoValidService = errors.New("no valid service found for: %w")
)
func loadPredefinedAliases() map[string][]string {
return map[string][]string{
"clam": {"clamav-daemon.service", "clamd@scan.service", "clamd"},
"freshclam": {"clamav-freshclam.service", "freshclam.service"},
"fail2ban": {"fail2ban.service", "fail2ban"},
"supervisor": {"supervisord.service", "supervisor.service", "supervisord", "supervisor"},
"ssh": {"sshd.service", "ssh.service", "sshd", "ssh"},
"1panel": {"1panel.service", "1paneld"},
"docker": {"docker.service", "dockerd"},
}
}
func InitializeServiceDiscovery() {
svcName := loadAliasesFromConfig()
if len(svcName) > 0 {
RegisterServiceAliases(svcName)
}
}
func RegisterServiceAliases(aliases map[string][]string) {
for key, values := range aliases {
existing, loaded := serviceAliases.LoadOrStore(key, values)
if loaded {
merged := append(existing.([]string), values...)
serviceAliases.Store(key, merged)
}
}
}
func loadAliasesFromConfig() map[string][]string {
data, err := os.ReadFile(aliasFile)
if err != nil {
return nil
}
var rawAliases map[string][]string
json.Unmarshal(data, &rawAliases)
validAliases := make(map[string][]string)
for key, aliases := range rawAliases {
valid := []string{}
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) > 0 {
validAliases[key] = valid
}
}
return validAliases
}
func cleanupKeywordAliases(keyword string) {
serviceAliases.Range(func(k, v interface{}) bool {
if k.(string) != keyword {
return true
}
aliases := v.([]string)
valid := make([]string, 0)
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) == 0 {
serviceAliases.Delete(k)
serviceExistenceCache.Delete(k)
} else {
serviceAliases.Store(k, valid)
}
return true
})
go scheduleSave()
}
func smartServiceName(keyword string) (string, error) {
mgr := GetGlobalManager()
processedName := handleServiceNaming(mgr, keyword)
confirmed, _ := confirmServiceExists(processedName)
if confirmed {
updateAliases(keyword, processedName)
return processedName, nil
}
candidates := append([]string{processedName}, getAliases(keyword)...)
if name, err := validateCandidatesConcurrently(candidates); err == nil {
updateAliases(keyword, name)
return name, nil
}
discoveredName, err := discoverAndSelectService(keyword)
if err != nil {
cleanupKeywordAliases(keyword)
return "", ErrServiceNotFound
}
updateAliases(keyword, discoveredName)
return discoveredName, nil
}
func handleServiceNaming(mgr ServiceManager, keyword string) string {
keyword = strings.ToLower(keyword)
if strings.HasSuffix(keyword, ".service.socket") {
keyword = strings.TrimSuffix(keyword, ".service.socket") + ".socket"
}
if mgr.Name() != "systemd" {
keyword = strings.TrimSuffix(keyword, ".service")
return keyword
}
if !strings.HasSuffix(keyword, ".service") &&
!strings.HasSuffix(keyword, ".socket") {
keyword += ".service"
}
return keyword
}
func validateCandidatesConcurrently(candidates []string) (string, error) {
var (
g errgroup.Group
found = make(chan string, 1)
)
for _, candidate := range candidates {
cand := candidate
g.Go(func() error {
confirmed, _ := confirmServiceExists(cand)
if confirmed {
select {
case found <- cand:
default:
}
return nil
}
return ErrServiceNotFound
})
}
resultErr := make(chan error, 1)
go func() {
defer close(found)
resultErr <- g.Wait()
}()
select {
case name := <-found:
return name, nil
case <-time.After(1000 * time.Millisecond):
return "", fmt.Errorf(ErrDiscoveryTimeout.Error(), candidates[0])
case err := <-resultErr:
if err != nil {
return "", ErrServiceNotFound
}
return "", ErrServiceNotFound
}
}
func discoverAndSelectService(keyword string) (string, error) {
discovered, err := discoverServices(keyword)
if err != nil {
return "", ErrServiceNotFound
}
if len(discovered) == 0 {
return "", ErrServiceNotFound
}
selected, err := selectBestMatch(keyword, discovered)
if err != nil {
return "", ErrServiceNotFound
}
confirmed, err := confirmServiceExists(selected)
if err != nil {
return "", fmt.Errorf("service existence check failed: %w", err)
}
if confirmed {
return selected, nil
}
return "", ErrServiceNotFound
}
func selectBestMatch(keyword string, candidates []string) (string, error) {
if len(candidates) == 0 {
return "", ErrServiceNotFound
}
lowerKeyword := strings.ToLower(keyword)
var exactMatch string
var firstContainMatch string
for _, name := range candidates {
if strings.EqualFold(name, keyword) {
exactMatch = name
break
}
}
if exactMatch != "" {
return exactMatch, nil
}
for _, name := range candidates {
if strings.Contains(strings.ToLower(name), lowerKeyword) {
firstContainMatch = name
global.LOG.Debugf("[%s] [keyword: %s] Found first contain match: %s", getManagerName(), keyword, firstContainMatch)
break
}
}
if firstContainMatch != "" {
return firstContainMatch, nil
}
return "", fmt.Errorf("%w: %q (no exact or partial match)", ErrNoValidService, keyword)
}
type cacheItem struct {
services []string
expires time.Time
exists bool
}
var (
discoveryCache sync.Map
discoveryGroup singleflight.Group
)
func discoverServices(keyword string) ([]string, error) {
result, err := discoveryGroup.Do(keyword, func() (interface{}, error) {
if cached, ok := discoveryCache.Load(keyword); ok {
item := cached.(cacheItem)
if time.Now().Before(item.expires) {
return item.services, nil
}
discoveryCache.Delete(keyword)
}
manager := GetGlobalManager()
results, err := manager.FindServices(keyword)
if err != nil {
global.LOG.Errorf("Find services failed for %s: %v", keyword, err)
return nil, fmt.Errorf("%w: %q (%v)", ErrServiceDiscovery, keyword, err)
} else {
discoveryCache.Store(keyword, cacheItem{
services: results,
expires: time.Now().Add(5 * time.Minute),
})
}
return results, err
})
if err != nil {
return nil, err
}
return result.([]string), nil
}
func updateAliases(keyword, alias string) {
if keyword == alias {
return
}
existing, _ := serviceAliases.LoadOrStore(keyword, []string{})
aliases := existing.([]string)
if contains(aliases, alias) {
return
}
serviceAliases.Store(keyword, append(aliases, alias))
go scheduleSave()
}
func scheduleSave() {
saveMutex.Lock()
defer saveMutex.Unlock()
if saveTimer != nil {
saveTimer.Stop()
}
dataSnapshot := make(map[string][]string)
serviceAliases.Range(func(k, v interface{}) bool {
dataSnapshot[k.(string)] = append([]string{}, v.([]string)...)
return true
})
aliasFile = filepath.Join(constant.ResourceDir, "svcaliases.json")
saveTimer = time.AfterFunc(afterSaveTime, func() {
tmpFile := aliasFile + ".tmp"
if err := saveAliasesToFile(dataSnapshot, tmpFile); err == nil {
os.Rename(tmpFile, aliasFile)
}
})
}
func saveAliasesToFile(data map[string][]string, path string) error {
fileData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("serialization failed: %w", err)
}
if err := os.WriteFile(path, fileData, 0644); err != nil {
return fmt.Errorf("file write failed: %w", err)
}
return nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
var serviceExistenceCache sync.Map
func confirmServiceExists(serviceName string) (bool, error) {
if val, ok := serviceExistenceCache.Load(serviceName); ok {
if item, ok := val.(cacheItem); ok && time.Now().Before(item.expires) {
return item.exists, nil
}
serviceExistenceCache.Delete(serviceName)
}
handler := NewServiceHandler(defaultServiceConfig(serviceName))
isExist, err := handler.IsExists()
if err != nil {
return false, fmt.Errorf("check service existence failed: %w", err)
}
serviceExistenceCache.Store(serviceName, cacheItem{
exists: isExist.IsExists,
expires: time.Now().Add(30 * time.Second),
})
return isExist.IsExists, nil
}
func getAliases(keyword string) []string {
predefined := loadPredefinedAliases()[keyword]
runtimeAliases, _ := serviceAliases.LoadOrStore(keyword, []string{})
merged := make(map[string]struct{})
for _, alias := range predefined {
merged[alias] = struct{}{}
}
for _, alias := range runtimeAliases.([]string) {
merged[alias] = struct{}{}
}
result := make([]string, 0, len(merged))
for k := range merged {
result = append(result, k)
}
return result
}
type ConfigOption struct {
TailLines string
}
func ViewConfig(path string, opt ConfigOption) (string, error) {
var cmd []string
if opt.TailLines != "" && opt.TailLines != "0" {
cmd = []string{"tail", "-n", opt.TailLines, path}
} else {
cmd = []string{"cat", path}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
output, err := executeCommand(ctx, cmd[0], cmd[1:]...)
if err != nil {
return "", fmt.Errorf("view config failed: %w", err)
}
return string(output), nil
}