package data import ( "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify" "kra/internal/config" ) // watchConfig refreshes the in-memory config snapshot on file changes, while // /system/reloadSystem remains responsible // for rebuilding database, Redis, storage, and scheduled tasks. func (d *Data) watchConfig() func() { logger := d.logger() configPath := d.runtime.ConfigPath() if configPath == "" { return func() {} } absolute, err := filepath.Abs(configPath) if err != nil { logger.Error("resolve config watch path", "mod", "system", "error", err) return func() {} } watcher, err := fsnotify.NewWatcher() if err != nil { logger.Error("create config watcher", "mod", "system", "error", err) return func() {} } if err = watcher.Add(filepath.Dir(absolute)); err != nil { logger.Error("watch config directory", "mod", "system", "error", err) _ = watcher.Close() return func() {} } done := make(chan struct{}) finished := make(chan struct{}) var once sync.Once go func() { defer close(finished) var timer *time.Timer var timerC <-chan time.Time events := watcher.Events watchErrors := watcher.Errors for { select { case event, ok := <-events: if !ok { events = nil continue } if filepath.Clean(event.Name) != absolute || event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 { continue } if timer != nil { if !timer.Stop() { select { case <-timer.C: default: } } } if timer == nil { timer = time.NewTimer(100 * time.Millisecond) } else { timer.Reset(100 * time.Millisecond) } timerC = timer.C case <-timerC: // The debounce callback runs on this goroutine so shutdown can // wait for it to finish without racing runtime replacement. timerC = nil select { case <-done: continue default: } next, loadErr := readBootstrap(absolute) if loadErr != nil { logger.Error("reload changed config", "mod", "system", "error", loadErr) continue } if next.Data == nil { logger.Error("reload changed config: data configuration is required", "mod", "system") continue } if next.Admin == nil { next.Admin = &config.Admin{} } if current := d.runtime.Admin(); current != nil { next.Admin.Storage = current.Storage next.Admin.Email = current.Email } next.Admin.ConfigPath = absolute d.runtime.Replace(next) logger.Info("config file changed", "mod", "system", "path", absolute) case watchErr, ok := <-watchErrors: if !ok { watchErrors = nil continue } logger.Error("config watcher error", "mod", "system", "error", watchErr) case <-done: if timer != nil { if !timer.Stop() { select { case <-timer.C: default: } } } return } } }() return func() { once.Do(func() { close(done) _ = watcher.Close() <-finished }) } }