// SPDX-License-Identifier: GPL-3.0-or-later package rootcheck import ( "context" "sync" "time" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model" ) const runTimeout = 30 * time.Second // Manager schedules the expensive cross-view collection independently from // polling. One bounded check may run at a time; completed alerts return to the // Agent's existing cooldown, incident, and snapshot path on the next sweep. type Manager struct { Config config.Config Now func() time.Time Run func(context.Context, model.State) []model.Alert mu sync.Mutex lastRun time.Time running bool pending []model.Alert } func New(cfg config.Config) *Manager { manager := &Manager{Config: cfg, Now: time.Now} manager.Run = func(ctx context.Context, state model.State) []model.Alert { return Run(ctx, state, cfg.RootcheckPIDCap, cfg.RootcheckModuleAllow) } return manager } func (m *Manager) MaybeStart(now time.Time, state model.State) { if !m.Config.RootcheckEnabled { return } m.mu.Lock() defer m.mu.Unlock() if m.running || (!m.lastRun.IsZero() && now.Sub(m.lastRun) < m.Config.RootcheckInterval) { return } // Record the start before launching the goroutine. This prevents a fast // polling loop from scheduling duplicates while collection is still running // or while a timed-out external command unwinds. m.running, m.lastRun = true, now go m.run(state) } func (m *Manager) run(state model.State) { // Rootcheck compares several host views and may invoke ps; its deadline // makes that diagnostic work bounded even on a degraded host. ctx, cancel := context.WithTimeout(context.Background(), runTimeout) defer cancel() alerts := m.Run(ctx, state) m.mu.Lock() m.pending = append(m.pending, alerts...) m.running = false m.mu.Unlock() } func (m *Manager) Drain() []model.Alert { m.mu.Lock() defer m.mu.Unlock() alerts := append([]model.Alert(nil), m.pending...) m.pending = nil return alerts }