diff --git a/moonshark.go b/moonshark.go index 5d513f4..702bd42 100644 --- a/moonshark.go +++ b/moonshark.go @@ -242,10 +242,10 @@ func initRouter() error { // Set up the file watchers. func setupWatchers() error { - wmg = watchers.NewWatcherManager(watchers.DefaultPollInterval) + wmg = watchers.NewWatcherManager() // Router watcher - err := wmg.WatchDirectory(watchers.DirectoryWatcherConfig{ + err := wmg.WatchDirectory(watchers.WatcherConfig{ Dir: cfg.Dirs.Routes, Callback: rtr.Refresh, Recursive: true, @@ -258,9 +258,9 @@ func setupWatchers() error { // Libs watchers for _, dir := range cfg.Dirs.Libs { - err := wmg.WatchDirectory(watchers.DirectoryWatcherConfig{ + err := wmg.WatchDirectory(watchers.WatcherConfig{ Dir: dir, - EnhancedCallback: func(changes []watchers.FileChange) error { + Callback: func(changes []watchers.FileChange) error { for _, change := range changes { if !change.IsDeleted && strings.HasSuffix(change.Path, ".lua") { rnr.NotifyFileChanged(change.Path) diff --git a/router/router.go b/router/router.go index 590c9d1..b93aa62 100644 --- a/router/router.go +++ b/router/router.go @@ -1,6 +1,7 @@ package router import ( + "Moonshark/watchers" "errors" "os" "path/filepath" @@ -457,7 +458,7 @@ func (r *Router) GetBytecode(scriptPath string) []byte { } // Refresh rebuilds the router -func (r *Router) Refresh() error { +func (r *Router) Refresh(changes []watchers.FileChange) error { r.get = &node{} r.post = &node{} r.put = &node{} diff --git a/watchers/dir.go b/watchers/dir.go deleted file mode 100644 index 4a26753..0000000 --- a/watchers/dir.go +++ /dev/null @@ -1,212 +0,0 @@ -package watchers - -import ( - "fmt" - "os" - "path/filepath" - "sync" - "time" - - "Moonshark/logger" -) - -// Default debounce time between detected change and callback -const defaultDebounceTime = 300 * time.Millisecond - -// FileChange represents a detected file change -type FileChange struct { - Path string - IsNew bool - IsDeleted bool -} - -// FileInfo stores minimal metadata about a file for change detection -type FileInfo struct { - ModTime time.Time -} - -// DirectoryWatcher watches a specific directory for changes -type DirectoryWatcher struct { - // Directory to watch - dir string - - // Map of file paths to their metadata - files map[string]FileInfo - filesMu sync.RWMutex - - // Track changed files during a check cycle - changedFiles []FileChange - - // Enhanced callback that receives changes (optional) - enhancedCallback func([]FileChange) error - - // Configuration - callback func() error - debounceTime time.Duration - recursive bool - - // Debounce timer - debounceTimer *time.Timer - debouncing bool - debounceMu sync.Mutex - - // Error tracking - consecutiveErrors int - lastError error -} - -// DirectoryWatcherConfig contains configuration for a directory watcher -type DirectoryWatcherConfig struct { - Dir string // Directory to watch - Callback func() error // Callback function to call when changes are detected - DebounceTime time.Duration // Debounce time (0 means use default) - Recursive bool // Recursive watching (watch subdirectories) - EnhancedCallback func([]FileChange) error // Enhanced callback that receives file changes -} - -// scanDirectory builds the initial file list -func (w *DirectoryWatcher) scanDirectory() error { - w.filesMu.Lock() - defer w.filesMu.Unlock() - - w.files = make(map[string]FileInfo) - - return filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil // Skip files with errors - } - - if !w.recursive && info.IsDir() && path != w.dir { - return filepath.SkipDir - } - - w.files[path] = FileInfo{ - ModTime: info.ModTime(), - } - - return nil - }) -} - -// checkForChanges detects if any files have been added, modified, or deleted -func (w *DirectoryWatcher) checkForChanges() (bool, error) { - w.filesMu.RLock() - prevFileCount := len(w.files) - w.filesMu.RUnlock() - - newFiles := make(map[string]FileInfo) - changed := false - w.changedFiles = nil // Reset changed files list - - err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { - // Skip errors - if err != nil { - return nil - } - - if !w.recursive && info.IsDir() && path != w.dir { - return filepath.SkipDir - } - - currentInfo := FileInfo{ - ModTime: info.ModTime(), - } - newFiles[path] = currentInfo - - w.filesMu.RLock() - prevInfo, exists := w.files[path] - w.filesMu.RUnlock() - - if !exists { - changed = true - w.changedFiles = append(w.changedFiles, FileChange{ - Path: path, - IsNew: true, - }) - w.logDebug("File added: %s", path) - } else if currentInfo.ModTime != prevInfo.ModTime { - changed = true - w.changedFiles = append(w.changedFiles, FileChange{ - Path: path, - }) - w.logDebug("File changed: %s", path) - } - - return nil - }) - - // Only check for deleted files if needed - if err == nil && (!changed && len(newFiles) != prevFileCount) { - w.filesMu.RLock() - for path := range w.files { - if _, exists := newFiles[path]; !exists { - changed = true - w.changedFiles = append(w.changedFiles, FileChange{ - Path: path, - IsDeleted: true, - }) - w.logDebug("File deleted: %s", path) - break // We already know changes happened - } - } - w.filesMu.RUnlock() - } - - if changed { - w.filesMu.Lock() - w.files = newFiles - w.filesMu.Unlock() - } - - return changed, err -} - -// notifyChange triggers the callback with debouncing -func (w *DirectoryWatcher) notifyChange() { - w.debounceMu.Lock() - defer w.debounceMu.Unlock() - - if w.debouncing { - if w.debounceTimer != nil { - w.debounceTimer.Stop() - } - } else { - w.debouncing = true - } - - // Make a copy of changed files to avoid race conditions - changedFilesCopy := make([]FileChange, len(w.changedFiles)) - copy(changedFilesCopy, w.changedFiles) - - w.debounceTimer = time.AfterFunc(w.debounceTime, func() { - var err error - if w.enhancedCallback != nil { - err = w.enhancedCallback(changedFilesCopy) - } else if w.callback != nil { - err = w.callback() - } - - if err != nil { - w.logError("Callback error: %v", err) - } - - w.debounceMu.Lock() - w.debouncing = false - w.debounceMu.Unlock() - }) -} - -// logDebug logs a debug message with the watcher's directory prefix -func (w *DirectoryWatcher) logDebug(format string, args ...any) { - logger.Debugf("[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.Errorf("[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/watchers/manager.go b/watchers/manager.go index cfff110..93aa9c4 100644 --- a/watchers/manager.go +++ b/watchers/manager.go @@ -3,62 +3,40 @@ package watchers import ( "errors" "sync" - "time" "Moonshark/logger" ) -// DefaultPollInterval is the time between directory checks -const DefaultPollInterval = 1 * time.Second - var ( ErrDirectoryNotFound = errors.New("directory not found") ErrAlreadyWatching = errors.New("already watching directory") ) -// WatcherManager coordinates file watching across multiple directories +// WatcherManager now just manages watcher lifecycle - no polling logic type WatcherManager struct { - watchers map[string]*DirectoryWatcher + watchers map[string]*Watcher mu sync.RWMutex - - done chan struct{} - ticker *time.Ticker - interval time.Duration - - wg sync.WaitGroup } -// NewWatcherManager creates a new watcher manager with a specified poll interval -func NewWatcherManager(pollInterval time.Duration) *WatcherManager { - if pollInterval <= 0 { - pollInterval = DefaultPollInterval +func NewWatcherManager() *WatcherManager { + return &WatcherManager{ + watchers: make(map[string]*Watcher), } - - manager := &WatcherManager{ - watchers: make(map[string]*DirectoryWatcher), - done: make(chan struct{}), - interval: pollInterval, - } - - manager.ticker = time.NewTicker(pollInterval) - manager.wg.Add(1) - go manager.pollLoop() - - return manager } -// Close stops all watchers and the manager func (m *WatcherManager) Close() error { - close(m.done) - if m.ticker != nil { - m.ticker.Stop() + m.mu.Lock() + defer m.mu.Unlock() + + for _, watcher := range m.watchers { + watcher.Close() } - m.wg.Wait() + + m.watchers = make(map[string]*Watcher) return nil } -// WatchDirectory adds a new directory to watch and returns the watcher -func (m *WatcherManager) WatchDirectory(config DirectoryWatcherConfig) error { +func (m *WatcherManager) WatchDirectory(config WatcherConfig) error { m.mu.Lock() defer m.mu.Unlock() @@ -66,21 +44,8 @@ func (m *WatcherManager) WatchDirectory(config DirectoryWatcherConfig) error { return ErrAlreadyWatching } - if config.DebounceTime == 0 { - config.DebounceTime = defaultDebounceTime - } - - watcher := &DirectoryWatcher{ - dir: config.Dir, - files: make(map[string]FileInfo), - callback: config.Callback, - enhancedCallback: config.EnhancedCallback, - debounceTime: config.DebounceTime, - recursive: config.Recursive, - } - - // Perform initial scan - if err := watcher.scanDirectory(); err != nil { + watcher, err := NewWatcher(config) + if err != nil { return err } @@ -90,64 +55,38 @@ func (m *WatcherManager) WatchDirectory(config DirectoryWatcherConfig) error { return nil } -// 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 { + watcher, exists := m.watchers[dir] + if !exists { return ErrDirectoryNotFound } + watcher.Close() delete(m.watchers, dir) logger.Debugf("WatcherManager removed watcher for %s", dir) + return nil } -// pollLoop is the main polling loop that checks all watched directories -func (m *WatcherManager) pollLoop() { - defer m.wg.Done() - - for { - select { - case <-m.ticker.C: - m.checkAllDirectories() - case <-m.done: - return - } - } -} - -// checkAllDirectories polls all registered directories for changes -func (m *WatcherManager) checkAllDirectories() { +func (m *WatcherManager) GetWatcher(dir string) (*Watcher, bool) { m.mu.RLock() - watchers := make([]*DirectoryWatcher, 0, len(m.watchers)) - for _, w := range m.watchers { - watchers = append(watchers, w) - } - m.mu.RUnlock() + defer m.mu.RUnlock() - changesDetected := 0 - - for _, watcher := range watchers { - if watcher.consecutiveErrors > 3 { - if watcher.consecutiveErrors == 4 { - logger.Errorf("Temporarily skipping directory %s due to errors: %v", - watcher.dir, watcher.lastError) - watcher.consecutiveErrors++ - } - continue - } - - changed, err := watcher.checkForChanges() - if err != nil { - logger.Errorf("Error checking directory %s: %v", watcher.dir, err) - continue - } - - if changed { - changesDetected++ - watcher.notifyChange() - } - } + watcher, exists := m.watchers[dir] + return watcher, exists +} + +func (m *WatcherManager) ListWatching() []string { + m.mu.RLock() + defer m.mu.RUnlock() + + dirs := make([]string, 0, len(m.watchers)) + for dir := range m.watchers { + dirs = append(dirs, dir) + } + + return dirs }