simplify file watchers

This commit is contained in:
Sky Johnson 2025-07-01 21:13:33 -05:00
parent d86167a86e
commit 8f74566e96
4 changed files with 41 additions and 313 deletions

View File

@ -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)

View File

@ -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{}

View File

@ -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
}

View File

@ -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
}