add watch mode

This commit is contained in:
Sky Johnson 2025-07-17 23:00:47 -05:00
parent 88c9bd90af
commit cc2cb0c682
3 changed files with 250 additions and 9 deletions

View File

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

View File

@ -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 <script.lua>\n", filepath.Base(os.Args[0]))
flag.Parse()
if flag.NArg() < 1 {
fmt.Fprintf(os.Stderr, "Usage: %s [--watch|-w] <script.lua>\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
}
}
}

136
watcher.go Normal file
View File

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