package config import ( "fmt" "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify" "go.uber.org/zap" ) // ConfigWatcher surveille les fichiers de configuration pour changements (T0040) type ConfigWatcher struct { watcher *fsnotify.Watcher reloader *ConfigReloader logger *zap.Logger stopChan chan struct{} stopOnce sync.Once // Ensures stopChan is closed only once wg sync.WaitGroup debounce time.Duration } // NewConfigWatcher crée un nouveau watcher de configuration (T0040) func NewConfigWatcher(reloader *ConfigReloader, logger *zap.Logger) (*ConfigWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, fmt.Errorf("failed to create watcher: %w", err) } return &ConfigWatcher{ watcher: watcher, reloader: reloader, logger: logger, stopChan: make(chan struct{}), stopOnce: sync.Once{}, // Initialize sync.Once debounce: 500 * time.Millisecond, }, nil } // Watch surveille les fichiers .env pour changements (T0040) func (w *ConfigWatcher) Watch(envFiles []string) error { // Ajouter les fichiers à surveiller for _, file := range envFiles { // Résoudre le chemin absolu pour éviter les problèmes de chemins relatifs absPath, err := filepath.Abs(file) if err != nil { w.logger.Warn("Failed to resolve absolute path", zap.String("file", file), zap.Error(err)) absPath = file } if err := w.watcher.Add(absPath); err != nil { w.logger.Warn("Failed to watch file", zap.String("file", absPath), zap.Error(err)) continue } w.logger.Info("Watching config file", zap.String("file", absPath)) } w.wg.Add(1) go w.watchLoop() return nil } // watchLoop boucle principale de surveillance avec debouncing (T0040) func (w *ConfigWatcher) watchLoop() { defer w.wg.Done() var debounceTimer *time.Timer for { select { case event, ok := <-w.watcher.Events: if !ok { return } // Ignorer les opérations autres que Write et Create if event.Op&fsnotify.Write == 0 && event.Op&fsnotify.Create == 0 { continue } w.logger.Debug("Config file changed", zap.String("file", event.Name), zap.String("op", event.Op.String())) // Arrêter le timer précédent si existant if debounceTimer != nil { debounceTimer.Stop() } // Démarrer un nouveau timer de debounce debounceTimer = time.NewTimer(w.debounce) // Goroutine pour attendre le debounce et relancer go func(fileName string) { <-debounceTimer.C w.logger.Info("Config file changed, reloading", zap.String("file", fileName)) if err := w.reloader.ReloadAll(); err != nil { w.logger.Error("Failed to reload config", zap.Error(err)) } else { w.logger.Info("Config reloaded successfully") } }(event.Name) case err, ok := <-w.watcher.Errors: if !ok { return } w.logger.Error("Watcher error", zap.Error(err)) case <-w.stopChan: // Arrêter le timer si actif if debounceTimer != nil { debounceTimer.Stop() } return } } } // Stop arrête la surveillance proprement (T0040) func (w *ConfigWatcher) Stop() error { w.stopOnce.Do(func() { close(w.stopChan) }) err := w.watcher.Close() w.wg.Wait() return err } // GetWatchedFiles retourne la liste des fichiers surveillés (T0040) func (w *ConfigWatcher) GetWatchedFiles() []string { if w.watcher == nil { return []string{} } return w.watcher.WatchList() }