From cc2cb0c682496f91b263054a668e5c8bf4cfb43c Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 17 Jul 2025 23:00:47 -0500 Subject: [PATCH] add watch mode --- modules/http/http.go | 19 ++++++ moonshark.go | 104 ++++++++++++++++++++++++++++++--- watcher.go | 136 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 watcher.go diff --git a/modules/http/http.go b/modules/http/http.go index 4a80275..712beb7 100644 --- a/modules/http/http.go +++ b/modules/http/http.go @@ -308,3 +308,22 @@ func RegisterStaticHandler(urlPrefix, rootPath string) { staticHandlers[urlPrefix] = fs } + +func StopAllServers() { + globalMu.Lock() + defer globalMu.Unlock() + + if globalWorkerPool != nil { + globalWorkerPool.Close() + globalWorkerPool = nil + } + + if globalServer != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + globalServer.ShutdownWithContext(ctx) + globalServer = nil + } + + serverRunning = false +} diff --git a/moonshark.go b/moonshark.go index 5bf5a89..bbc8056 100644 --- a/moonshark.go +++ b/moonshark.go @@ -1,25 +1,43 @@ package main import ( + "flag" "fmt" + "log" "os" "os/signal" "path/filepath" "syscall" + "time" "Moonshark/modules/http" "Moonshark/state" ) +var ( + watchFlag = flag.Bool("watch", false, "Watch script files for changes and restart") + wFlag = flag.Bool("w", false, "Watch script files for changes and restart (short)") +) + func main() { - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "Usage: %s \n", filepath.Base(os.Args[0])) + flag.Parse() + + if flag.NArg() < 1 { + fmt.Fprintf(os.Stderr, "Usage: %s [--watch|-w] \n", filepath.Base(os.Args[0])) os.Exit(1) } - scriptPath := os.Args[1] + scriptPath := flag.Arg(0) + watchMode := *watchFlag || *wFlag - // Create state configured for the script + if watchMode { + runWithWatcher(scriptPath) + } else { + runOnce(scriptPath) + } +} + +func runOnce(scriptPath string) { luaState, err := state.NewFromScript(scriptPath) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -27,28 +45,96 @@ func main() { } defer luaState.Close() - // Execute the script if err := luaState.ExecuteFile(scriptPath); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } - // Check if HTTP servers are running if http.HasActiveServers() { - // Set up signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) fmt.Println("HTTP servers running. Press Ctrl+C to exit.") - // Wait for either signal or servers to close go func() { <-sigChan fmt.Println("\nShutting down...") os.Exit(0) }() - // Wait for all servers to finish http.WaitForServers() } } + +func runWithWatcher(scriptPath string) { + watcher, err := NewFileWatcher(500) // 500ms debounce + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create file watcher: %v\n", err) + os.Exit(1) + } + defer watcher.Close() + + if err := watcher.DiscoverRequiredFiles(scriptPath); err != nil { + fmt.Fprintf(os.Stderr, "Failed to watch files: %v\n", err) + os.Exit(1) + } + + restartCh := watcher.Start() + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + fmt.Printf("Starting %s in watch mode...\n", scriptPath) + + for { + // Clear cache before each run + state.ClearCache() + + // Create and run state + luaState, err := state.NewFromScript(scriptPath) + if err != nil { + log.Printf("Error creating state: %v", err) + time.Sleep(1 * time.Second) + continue + } + + if err := luaState.ExecuteFile(scriptPath); err != nil { + log.Printf("Execution error: %v", err) + luaState.Close() + time.Sleep(1 * time.Second) + continue + } + + // If not a long-running process, wait for changes and restart + if !http.HasActiveServers() { + fmt.Println("Script completed. Waiting for changes...") + luaState.Close() + + select { + case <-restartCh: + fmt.Println("Files changed, restarting...") + continue + case <-sigChan: + fmt.Println("\nExiting...") + return + } + } + + // Long-running process - wait for restart signal or exit signal + fmt.Println("HTTP servers running. Watching for file changes...") + + select { + case <-restartCh: + fmt.Println("Files changed, restarting...") + http.StopAllServers() + luaState.Close() + time.Sleep(100 * time.Millisecond) // Brief pause for cleanup + continue + + case <-sigChan: + fmt.Println("\nShutting down...") + http.StopAllServers() + luaState.Close() + return + } + } +} diff --git a/watcher.go b/watcher.go new file mode 100644 index 0000000..ca4271a --- /dev/null +++ b/watcher.go @@ -0,0 +1,136 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +type FileWatcher struct { + files map[string]time.Time + 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), + 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 { + return filepath.Walk(dir, 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() + } + } +} + +func (fw *FileWatcher) checkFiles() { + fw.mu.RLock() + files := make(map[string]time.Time, len(fw.files)) + for path, modTime := range fw.files { + files[path] = modTime + } + 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) 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) +}