package main import ( "fmt" "maps" "os" "path/filepath" "strings" "sync" "time" ) type FileWatcher struct { files map[string]time.Time dirs map[string]bool // Track watched directories mu sync.RWMutex restartCh chan bool stopCh chan bool debounceMs int lastEvent time.Time pollMs int } func NewFileWatcher(debounceMs int) (*FileWatcher, error) { return &FileWatcher{ files: make(map[string]time.Time), dirs: make(map[string]bool), restartCh: make(chan bool, 1), stopCh: make(chan bool, 1), debounceMs: debounceMs, pollMs: 250, // Poll every 250ms }, nil } func (fw *FileWatcher) AddFile(path string) error { absPath, err := filepath.Abs(path) if err != nil { return err } info, err := os.Stat(absPath) if err != nil { return err } fw.mu.Lock() fw.files[absPath] = info.ModTime() fw.mu.Unlock() fmt.Printf("Watching: %s\n", absPath) return nil } func (fw *FileWatcher) AddDirectory(dir string) error { absDir, err := filepath.Abs(dir) if err != nil { return err } fw.mu.Lock() fw.dirs[absDir] = true fw.mu.Unlock() return filepath.Walk(absDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(path, ".lua") { return fw.AddFile(path) } return nil }) } func (fw *FileWatcher) Start() <-chan bool { go fw.pollLoop() return fw.restartCh } func (fw *FileWatcher) pollLoop() { ticker := time.NewTicker(time.Duration(fw.pollMs) * time.Millisecond) defer ticker.Stop() for { select { case <-fw.stopCh: return case <-ticker.C: fw.checkFiles() fw.scanForNewFiles() } } } func (fw *FileWatcher) checkFiles() { fw.mu.RLock() files := make(map[string]time.Time, len(fw.files)) maps.Copy(files, fw.files) fw.mu.RUnlock() changed := false for path, lastMod := range files { info, err := os.Stat(path) if err != nil { continue } if info.ModTime().After(lastMod) { fw.mu.Lock() fw.files[path] = info.ModTime() fw.mu.Unlock() now := time.Now() if now.Sub(fw.lastEvent) > time.Duration(fw.debounceMs)*time.Millisecond { fw.lastEvent = now // log.Printf("File changed: %s", path) changed = true } } } if changed { select { case fw.restartCh <- true: default: } } } func (fw *FileWatcher) scanForNewFiles() { fw.mu.RLock() dirs := make(map[string]bool, len(fw.dirs)) maps.Copy(dirs, fw.dirs) existingFiles := make(map[string]bool, len(fw.files)) for path := range fw.files { existingFiles[path] = true } fw.mu.RUnlock() newFilesFound := false for dir := range dirs { err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(path, ".lua") { absPath, err := filepath.Abs(path) if err != nil { return err } if !existingFiles[absPath] { fw.mu.Lock() fw.files[absPath] = info.ModTime() fw.mu.Unlock() fmt.Printf("New file detected: %s\n", absPath) newFilesFound = true } } return nil }) if err != nil { continue } } if newFilesFound { now := time.Now() if now.Sub(fw.lastEvent) > time.Duration(fw.debounceMs)*time.Millisecond { fw.lastEvent = now select { case fw.restartCh <- true: default: } } } } func (fw *FileWatcher) Close() error { select { case fw.stopCh <- true: default: } return nil } func (fw *FileWatcher) DiscoverRequiredFiles(scriptPath string) error { if err := fw.AddFile(scriptPath); err != nil { return err } scriptDir := filepath.Dir(scriptPath) return fw.AddDirectory(scriptDir) }