package collector import ( "bytes" "context" "log" "os/exec" "strings" "sync" "time" "codeberg.org/pata1704/guenther/pkg/types" ) // SystemctlCollector periodically checks the status of systemd services. type SystemctlCollector struct { services []string interval time.Duration outputChan chan<- types.ServiceStatus healthChan chan<- types.StageHealth wg sync.WaitGroup mu sync.Mutex processed uint64 } // NewSystemctlCollector creates a new collector for the given services. func NewSystemctlCollector( services []string, interval time.Duration, output chan<- types.ServiceStatus, health chan<- types.StageHealth, ) *SystemctlCollector { return &SystemctlCollector{ services: services, interval: interval, outputChan: output, healthChan: health, } } // Start launches the collection loop. func (c *SystemctlCollector) Start(ctx context.Context) { if len(c.services) == 0 { log.Println("systemctl: no services configured for monitoring") return } c.wg.Go(func() { ticker := time.NewTicker(c.interval) reportTicker := time.NewTicker(5 * time.Second) defer ticker.Stop() defer reportTicker.Stop() // Immediate first collection. c.collect() for { select { case <-ctx.Done(): return case <-ticker.C: c.collect() case <-reportTicker.C: c.emitHealth() } } }) } // Wait waits for the collector to stop. func (c *SystemctlCollector) Wait() { c.wg.Wait() } func (c *SystemctlCollector) collect() { for _, service := range c.services { status, err := c.getServiceStatus(service) if err != nil { log.Printf("systemctl: error getting status for %s: %v", service, err) continue } select { case c.outputChan <- status: c.mu.Lock() c.processed++ c.mu.Unlock() default: log.Printf("systemctl: output channel full – dropping status for %s", service) } } } func (c *SystemctlCollector) getServiceStatus(service string) (types.ServiceStatus, error) { // Use systemctl show to get machine-readable properties. cmd := exec.Command("systemctl", "show", "-p", "ActiveState,SubState", service) var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { return types.ServiceStatus{}, err } lines := strings.Split(strings.TrimSpace(out.String()), "\n") status := types.ServiceStatus{ Timestamp: time.Now(), ServiceName: service, } for _, line := range lines { parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } switch parts[0] { case "ActiveState": status.ActiveState = parts[1] case "SubState": status.SubState = parts[1] } } return status, nil } func (c *SystemctlCollector) emitHealth() { c.mu.Lock() count := c.processed c.mu.Unlock() select { case c.healthChan <- types.StageHealth{ StageName: "systemctl_collector", EventsProcessed: count, LastUpdate: time.Now(), }: default: } }