Moonshark/watcher.go

198 lines
3.6 KiB
Go

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