package watcher import ( "crypto/md5" "fmt" "os" "time" "firewall_containers/network-go/logger" ) // FileWatcher periodically polls a file for changes AND triggers a periodic // reconciliation callback regardless of file changes. type FileWatcher struct { path string period time.Duration lastHash string onChange func() stopCh chan struct{} } // NewFileWatcher creates a new file watcher that: // 1. Polls the file for content changes at the given period // 2. Triggers a reconciliation callback every period regardless of changes // to ensure desired state is maintained (stateful reconciliation) func NewFileWatcher(path string, period time.Duration, onChange func()) *FileWatcher { return &FileWatcher{ path: path, period: period, lastHash: "", onChange: onChange, stopCh: make(chan struct{}), } } // hashFile computes an MD5 hash of the file contents func (fw *FileWatcher) hashFile() (string, error) { data, err := os.ReadFile(fw.path) if err != nil { return "", fmt.Errorf("failed to read file %s: %w", fw.path, err) } return fmt.Sprintf("%x", md5.Sum(data)), nil } // Start begins polling the file for changes in a goroutine. // Every period, it checks if the file changed AND triggers a full reconciliation // to maintain the desired state (handles container restarts, iptables flushes, etc.) func (fw *FileWatcher) Start() { // Compute initial hash hash, err := fw.hashFile() if err != nil { logger.Warn("WATCHER: initial hash computation failed for %s: %v", fw.path, err) } else { fw.lastHash = hash } go func() { ticker := time.NewTicker(fw.period) defer ticker.Stop() logger.Info("WATCHER: started watching %s every %s (periodic reconciliation enabled)", fw.path, fw.period) for { select { case <-fw.stopCh: logger.Info("WATCHER: stopped watching %s", fw.path) return case <-ticker.C: hash, err := fw.hashFile() if err != nil { logger.Warn("WATCHER: failed to hash %s: %v", fw.path, err) continue } fileChanged := hash != fw.lastHash if fileChanged { logger.Info("WATCHER: detected change in %s", fw.path) fw.lastHash = hash } // Trigger reconciliation every period to maintain state, // even if the config file hasn't changed. // This ensures container restarts, iptable flushes, etc. // are corrected. if fw.onChange != nil { if fileChanged { logger.Info("WATCHER: triggering reconciliation (config changed)") } else { logger.Debug("WATCHER: triggering periodic state reconciliation") } fw.onChange() } } } }() } // Stop signals the watcher goroutine to stop func (fw *FileWatcher) Stop() { close(fw.stopCh) }