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 }