package config import ( "context" "log" "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify" ) type Manager struct { mu sync.RWMutex config *Config watcher *fsnotify.Watcher wg sync.WaitGroup onConfigReload func() // Debouncer for config reloads debounceTimer *time.Timer debounceMutex sync.Mutex debounceDelay time.Duration } func NewManager() (*Manager, error) { log.Printf("Config manager: initializing configuration system...") config, err := Load() if err != nil { log.Printf("Config manager: failed to load initial configuration: %v", err) return nil, err } log.Printf("Config manager: validating initial configuration...") if err := config.Validate(); err != nil { log.Printf("Config manager: validation warning: %v", err) } m := &Manager{ config: config, debounceDelay: 500 * time.Millisecond, // 500ms debounce delay } log.Printf("Config manager: initialization completed successfully") return m, nil } func (m *Manager) GetConfig() *Config { m.mu.RLock() defer m.mu.RUnlock() // Return a copy to prevent external modification configCopy := *m.config return &configCopy } func (m *Manager) StartWatching(ctx context.Context) error { configPath, err := GetConfigPath() if err != nil { return err } watcher, err := fsnotify.NewWatcher() if err != nil { return err } m.watcher = watcher configDir := filepath.Dir(configPath) err = watcher.Add(configDir) if err != nil { watcher.Close() return err } m.wg.Add(1) go m.watchLoop(ctx, configPath) log.Printf("Config manager: watching %s for changes", configPath) return nil } func (m *Manager) Stop() { if m.watcher != nil { m.watcher.Close() } // Clean up debounce timer m.debounceMutex.Lock() if m.debounceTimer != nil { m.debounceTimer.Stop() } m.debounceMutex.Unlock() m.wg.Wait() } func (m *Manager) watchLoop(ctx context.Context, configPath string) { defer m.wg.Done() configFileName := filepath.Base(configPath) for { select { case event, ok := <-m.watcher.Events: if !ok { return } // Filter for our config file only eventFileName := filepath.Base(event.Name) if eventFileName != configFileName { continue } // Only react to Write and Create events (ignore Chmod, Remove, etc.) if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create { log.Printf("Config manager: file change detected: %s. Debouncing reload...", event.Name) m.debounceReloadConfig() } case err, ok := <-m.watcher.Errors: if !ok { return } log.Printf("Config watcher error: %v", err) case <-ctx.Done(): return } } } func (m *Manager) reloadConfig() { log.Printf("Config manager: starting configuration reload...") newConfig, err := Load() if err != nil { log.Printf("Config manager: failed to reload config: %v", err) return } log.Printf("Config manager: validating new configuration...") if err := newConfig.Validate(); err != nil { log.Printf("Config manager: invalid config after reload: %v", err) return } m.mu.Lock() m.config = newConfig onConfigReload := m.onConfigReload m.mu.Unlock() if onConfigReload != nil { onConfigReload() } log.Printf("Config manager: configuration successfully reloaded") } func (m *Manager) SetOnConfigReload(onConfigReload func()) { m.mu.Lock() defer m.mu.Unlock() m.onConfigReload = onConfigReload } // debounceReloadConfig implements debouncing to prevent duplicate reloads func (m *Manager) debounceReloadConfig() { m.debounceMutex.Lock() defer m.debounceMutex.Unlock() // Cancel existing timer if it exists if m.debounceTimer != nil { m.debounceTimer.Stop() } // Create new timer with debounce delay m.debounceTimer = time.AfterFunc(m.debounceDelay, func() { log.Printf("Config manager: debounce period expired, reloading config...") m.reloadConfig() }) }