diff --git a/core/moonshark.go b/core/moonshark.go index 13dd491..4e75feb 100644 --- a/core/moonshark.go +++ b/core/moonshark.go @@ -199,12 +199,17 @@ func (s *Moonshark) initRunner() error { // setupWatchers initializes and starts all configured file watchers func (s *Moonshark) setupWatchers() error { + manager := watchers.GetWatcherManager() + // Set up watcher for Lua routes - luaRouterWatcher, err := watchers.WatchLuaRouter(s.LuaRouter, s.LuaRunner, s.Config.Dirs.Routes) + routeWatcher, err := watchers.WatchLuaRouter(s.LuaRouter, s.LuaRunner, s.Config.Dirs.Routes) if err != nil { logger.Warning("Failed to watch routes directory: %v", err) } else { - s.cleanupFuncs = append(s.cleanupFuncs, luaRouterWatcher.Close) + routesDir := routeWatcher.GetDir() + s.cleanupFuncs = append(s.cleanupFuncs, func() error { + return manager.UnwatchDirectory(routesDir) + }) } // Set up watchers for Lua modules libraries @@ -213,8 +218,10 @@ func (s *Moonshark) setupWatchers() error { logger.Warning("Failed to watch Lua module directories: %v", err) } else { for _, watcher := range moduleWatchers { - w := watcher // Capture variable for closure - s.cleanupFuncs = append(s.cleanupFuncs, w.Close) + dirPath := watcher.GetDir() + s.cleanupFuncs = append(s.cleanupFuncs, func() error { + return manager.UnwatchDirectory(dirPath) + }) } logger.Info("File watchers active for %d Lua module directories", len(moduleWatchers)) } diff --git a/core/watchers/api.go b/core/watchers/api.go index 8c7a821..958cc78 100644 --- a/core/watchers/api.go +++ b/core/watchers/api.go @@ -9,56 +9,31 @@ import ( "Moonshark/core/utils/logger" ) -// Global watcher manager instance +// Global watcher manager instance with explicit creation var ( globalManager *WatcherManager globalManagerOnce sync.Once ) // GetWatcherManager returns the global watcher manager, creating it if needed -func GetWatcherManager(adaptive bool) *WatcherManager { +func GetWatcherManager() *WatcherManager { globalManagerOnce.Do(func() { - globalManager = NewWatcherManager() + globalManager = NewWatcherManager(DefaultPollInterval) }) return globalManager } -// WatchDirectory creates a new directory watcher and registers it with the manager -func WatchDirectory(config DirectoryWatcherConfig, manager *WatcherManager) (*Watcher, error) { - dirWatcher, err := NewDirectoryWatcher(config) - if err != nil { - return nil, fmt.Errorf("failed to create directory watcher: %w", err) +// ShutdownWatcherManager closes the global watcher manager if it exists +func ShutdownWatcherManager() { + if globalManager != nil { + globalManager.Close() + globalManager = nil } - - manager.AddWatcher(dirWatcher) - - // Create a wrapper Watcher that implements the old interface - w := &Watcher{ - dir: config.Dir, - dirWatch: dirWatcher, - manager: manager, - } - - return w, nil } -// Watcher is a compatibility wrapper that maintains the old API -type Watcher struct { - dir string - dirWatch *DirectoryWatcher - manager *WatcherManager -} - -// Close unregisters the watcher from the manager -func (w *Watcher) Close() error { - w.manager.RemoveWatcher(w.dir) - return nil -} - -// WatchLuaRouter sets up a watcher for a LuaRouter's routes directory; also updates -// the LuaRunner so that the state can be rebuilt -func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir string) (*Watcher, error) { - manager := GetWatcherManager(true) +// WatchLuaRouter sets up a watcher for a LuaRouter's routes directory +func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) { + manager := GetWatcherManager() runnerRefresh := func() error { logger.Debug("Refreshing LuaRunner state due to file change") @@ -74,9 +49,9 @@ func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir Recursive: true, } - watcher, err := WatchDirectory(config, manager) + watcher, err := manager.WatchDirectory(config) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to watch directory: %w", err) } logger.Info("Started watching Lua routes directory: %s", routesDir) @@ -84,13 +59,12 @@ func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir } // WatchLuaModules sets up watchers for Lua module directories -func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*Watcher, error) { - manager := GetWatcherManager(true) - watchers := make([]*Watcher, 0, len(libDirs)) +func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*DirectoryWatcher, error) { + manager := GetWatcherManager() + watchers := make([]*DirectoryWatcher, 0, len(libDirs)) for _, dir := range libDirs { - // Create a directory-specific callback - dirCopy := dir // Capture for closure + dirCopy := dir callback := func() error { logger.Debug("Detected changes in Lua module directory: %s", dirCopy) @@ -108,13 +82,12 @@ func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*Watcher, er Recursive: true, } - watcher, err := WatchDirectory(config, manager) + watcher, err := manager.WatchDirectory(config) if err != nil { - // Clean up already created watchers for _, w := range watchers { - w.Close() + manager.UnwatchDirectory(w.dir) } - return nil, err + return nil, fmt.Errorf("failed to watch directory %s: %w", dir, err) } watchers = append(watchers, watcher) @@ -124,14 +97,6 @@ func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*Watcher, er return watchers, nil } -// ShutdownWatcherManager closes the global watcher manager if it exists -func ShutdownWatcherManager() { - if globalManager != nil { - globalManager.Close() - globalManager = nil - } -} - // combineCallbacks creates a single callback function from multiple callbacks func combineCallbacks(callbacks ...func() error) func() error { return func() error { diff --git a/core/watchers/dir.go b/core/watchers/dir.go index 1d0528e..b4e8d4c 100644 --- a/core/watchers/dir.go +++ b/core/watchers/dir.go @@ -13,11 +13,9 @@ import ( // Default debounce time between detected change and callback const defaultDebounceTime = 300 * time.Millisecond -// FileInfo stores metadata about a file for change detection +// FileInfo stores minimal metadata about a file for change detection type FileInfo struct { ModTime time.Time - Size int64 - IsDir bool } // DirectoryWatcher watches a specific directory for changes @@ -38,6 +36,10 @@ type DirectoryWatcher struct { debounceTimer *time.Timer debouncing bool debounceMu sync.Mutex + + // Error tracking + consecutiveErrors int + lastError error } // DirectoryWatcherConfig contains configuration for a directory watcher @@ -48,35 +50,11 @@ type DirectoryWatcherConfig struct { Recursive bool // Recursive watching (watch subdirectories) } -// NewDirectoryWatcher creates a new directory watcher -func NewDirectoryWatcher(config DirectoryWatcherConfig) (*DirectoryWatcher, error) { - debounceTime := config.DebounceTime - if debounceTime == 0 { - debounceTime = defaultDebounceTime - } - - w := &DirectoryWatcher{ - dir: config.Dir, - files: make(map[string]FileInfo), - callback: config.Callback, - debounceTime: debounceTime, - recursive: config.Recursive, - } - - // Perform initial scan - if err := w.scanDirectory(); err != nil { - return nil, err - } - - return w, nil -} - // scanDirectory builds the initial file list func (w *DirectoryWatcher) scanDirectory() error { w.filesMu.Lock() defer w.filesMu.Unlock() - // Clear existing files map w.files = make(map[string]FileInfo) return filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { @@ -84,15 +62,12 @@ func (w *DirectoryWatcher) scanDirectory() error { return nil // Skip files with errors } - // Skip subdirectories if not recursive if !w.recursive && info.IsDir() && path != w.dir { return filepath.SkipDir } w.files[path] = FileInfo{ ModTime: info.ModTime(), - Size: info.Size(), - IsDir: info.IsDir(), } return nil @@ -101,42 +76,34 @@ func (w *DirectoryWatcher) scanDirectory() error { // checkForChanges detects if any files have been added, modified, or deleted func (w *DirectoryWatcher) checkForChanges() (bool, error) { - // Lock for reading previous state w.filesMu.RLock() prevFileCount := len(w.files) w.filesMu.RUnlock() - // Track new state and whether changes were detected newFiles := make(map[string]FileInfo) changed := false - // Walk the directory to check for changes err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { // Skip errors (file might have been deleted) if err != nil { return nil } - // Skip subdirectories if not recursive if !w.recursive && info.IsDir() && path != w.dir { return filepath.SkipDir } - // Store current file info currentInfo := FileInfo{ ModTime: info.ModTime(), - Size: info.Size(), - IsDir: info.IsDir(), } newFiles[path] = currentInfo - // Check if file is new or modified (only if we haven't already detected a change) if !changed { w.filesMu.RLock() prevInfo, exists := w.files[path] w.filesMu.RUnlock() - if !exists || currentInfo.ModTime != prevInfo.ModTime || currentInfo.Size != prevInfo.Size { + if !exists || currentInfo.ModTime != prevInfo.ModTime { changed = true w.logDebug("File changed: %s", path) } @@ -146,10 +113,13 @@ func (w *DirectoryWatcher) checkForChanges() (bool, error) { }) if err != nil { + w.consecutiveErrors++ + w.lastError = err return false, err } - // Check for deleted files (only if we haven't already detected a change) + w.consecutiveErrors = 0 + if !changed && len(newFiles) != prevFileCount { w.filesMu.RLock() for path := range w.files { @@ -162,7 +132,6 @@ func (w *DirectoryWatcher) checkForChanges() (bool, error) { w.filesMu.RUnlock() } - // Update files map if changed if changed { w.filesMu.Lock() w.files = newFiles @@ -177,7 +146,6 @@ func (w *DirectoryWatcher) notifyChange() { w.debounceMu.Lock() defer w.debounceMu.Unlock() - // Reset timer if already debouncing if w.debouncing { if w.debounceTimer != nil { w.debounceTimer.Stop() @@ -191,7 +159,6 @@ func (w *DirectoryWatcher) notifyChange() { w.logError("Callback error: %v", err) } - // Reset debouncing state w.debounceMu.Lock() w.debouncing = false w.debounceMu.Unlock() @@ -203,12 +170,12 @@ func (w *DirectoryWatcher) logDebug(format string, args ...any) { logger.Debug("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...)) } -// logWarning logs a warning message with the watcher's directory prefix -func (w *DirectoryWatcher) logWarning(format string, args ...any) { - logger.Warning("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...)) -} - // logError logs an error message with the watcher's directory prefix func (w *DirectoryWatcher) logError(format string, args ...any) { logger.Error("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...)) } + +// GetDir gets the DirectoryWatcher's current directory +func (w *DirectoryWatcher) GetDir() string { + return w.dir +} diff --git a/core/watchers/manager.go b/core/watchers/manager.go index b995785..87b6356 100644 --- a/core/watchers/manager.go +++ b/core/watchers/manager.go @@ -1,15 +1,20 @@ package watchers import ( + "errors" "sync" "time" "Moonshark/core/utils/logger" ) -// Default polling interval -const ( - defaultPollInterval = 1 * time.Second +// DefaultPollInterval is the time between directory checks +const DefaultPollInterval = 1 * time.Second + +// Common errors +var ( + ErrDirectoryNotFound = errors.New("directory not found") + ErrAlreadyWatching = errors.New("already watching directory") ) // WatcherManager coordinates file watching across multiple directories @@ -17,20 +22,26 @@ type WatcherManager struct { watchers map[string]*DirectoryWatcher mu sync.RWMutex - done chan struct{} - ticker *time.Ticker + done chan struct{} + ticker *time.Ticker + interval time.Duration wg sync.WaitGroup } -// NewWatcherManager creates a new watcher manager -func NewWatcherManager() *WatcherManager { +// NewWatcherManager creates a new watcher manager with a specified poll interval +func NewWatcherManager(pollInterval time.Duration) *WatcherManager { + if pollInterval <= 0 { + pollInterval = DefaultPollInterval + } + manager := &WatcherManager{ watchers: make(map[string]*DirectoryWatcher), done: make(chan struct{}), + interval: pollInterval, } - manager.ticker = time.NewTicker(defaultPollInterval) + manager.ticker = time.NewTicker(pollInterval) manager.wg.Add(1) go manager.pollLoop() @@ -47,22 +58,50 @@ func (m *WatcherManager) Close() error { return nil } -// AddWatcher registers a new directory watcher -func (m *WatcherManager) AddWatcher(watcher *DirectoryWatcher) { +// WatchDirectory adds a new directory to watch and returns the watcher +func (m *WatcherManager) WatchDirectory(config DirectoryWatcherConfig) (*DirectoryWatcher, error) { m.mu.Lock() defer m.mu.Unlock() - m.watchers[watcher.dir] = watcher - logger.Debug("WatcherManager added watcher for %s", watcher.dir) + if _, exists := m.watchers[config.Dir]; exists { + return nil, ErrAlreadyWatching + } + + if config.DebounceTime == 0 { + config.DebounceTime = defaultDebounceTime + } + + watcher := &DirectoryWatcher{ + dir: config.Dir, + files: make(map[string]FileInfo), + callback: config.Callback, + debounceTime: config.DebounceTime, + recursive: config.Recursive, + } + + // Perform initial scan + if err := watcher.scanDirectory(); err != nil { + return nil, err + } + + m.watchers[config.Dir] = watcher + logger.Debug("WatcherManager added watcher for %s", config.Dir) + + return watcher, nil } -// RemoveWatcher unregisters a directory watcher -func (m *WatcherManager) RemoveWatcher(dir string) { +// UnwatchDirectory removes a directory from being watched +func (m *WatcherManager) UnwatchDirectory(dir string) error { m.mu.Lock() defer m.mu.Unlock() + if _, exists := m.watchers[dir]; !exists { + return ErrDirectoryNotFound + } + delete(m.watchers, dir) logger.Debug("WatcherManager removed watcher for %s", dir) + return nil } // pollLoop is the main polling loop that checks all watched directories @@ -82,16 +121,32 @@ func (m *WatcherManager) pollLoop() { // checkAllDirectories polls all registered directories for changes func (m *WatcherManager) checkAllDirectories() { m.mu.RLock() - defer m.mu.RUnlock() + watchers := make([]*DirectoryWatcher, 0, len(m.watchers)) + for _, w := range m.watchers { + watchers = append(watchers, w) + } + m.mu.RUnlock() + + changesDetected := 0 + + for _, watcher := range watchers { + if watcher.consecutiveErrors > 3 { + if watcher.consecutiveErrors == 4 { + logger.Error("Temporarily skipping directory %s due to errors: %v", + watcher.dir, watcher.lastError) + watcher.consecutiveErrors++ + } + continue + } - for _, watcher := range m.watchers { changed, err := watcher.checkForChanges() if err != nil { - logger.Error("[WatcherManager] Error checking directory %s: %v", watcher.dir, err) + logger.Error("Error checking directory %s: %v", watcher.dir, err) continue } if changed { + changesDetected++ watcher.notifyChange() } }