From 3bece4663890a1f46b53c8b46de3e81e9b69b0c6 Mon Sep 17 00:00:00 2001 From: Michael Wolf Date: Thu, 1 Jan 2026 12:26:54 -0600 Subject: [PATCH] Add first cut at golang monitor program --- monitor/.gitignore | 1 + monitor/devrunner.go | 84 +++++++++++++++++++++++++++++++++++++++++ monitor/filechange.go | 6 +++ monitor/go.mod | 8 ++++ monitor/go.sum | 4 ++ monitor/main.go | 50 ++++++++++++++++++++++++ monitor/printchanges.go | 11 ++++++ monitor/watchfiles.go | 74 ++++++++++++++++++++++++++++++++++++ 8 files changed, 238 insertions(+) create mode 100644 monitor/.gitignore create mode 100644 monitor/devrunner.go create mode 100644 monitor/filechange.go create mode 100644 monitor/go.mod create mode 100644 monitor/go.sum create mode 100644 monitor/main.go create mode 100644 monitor/printchanges.go create mode 100644 monitor/watchfiles.go diff --git a/monitor/.gitignore b/monitor/.gitignore new file mode 100644 index 0000000..edffd38 --- /dev/null +++ b/monitor/.gitignore @@ -0,0 +1 @@ +monitor \ No newline at end of file diff --git a/monitor/devrunner.go b/monitor/devrunner.go new file mode 100644 index 0000000..81ccd36 --- /dev/null +++ b/monitor/devrunner.go @@ -0,0 +1,84 @@ +// a vibe coded el cheapo: https://claude.ai/chat/328ca558-1019-49b9-9f08-e85cfcea2ceb + +package main + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "sync" + "time" +) + +func runProcess(ctx context.Context, wg *sync.WaitGroup, name, command string) { + defer wg.Done() + + for { + select { + case <-ctx.Done(): + fmt.Printf("[%s] Stopping\n", name) + return + default: + fmt.Printf("[%s] Starting: %s\n", name, command) + + // Create command with context for cancellation + cmd := exec.CommandContext(ctx, "sh", "-c", command) + + // Setup stdout pipe + stdout, err := cmd.StdoutPipe() + if err != nil { + fmt.Fprintf(os.Stderr, "[%s] Error creating stdout pipe: %v\n", name, err) + return + } + + // Setup stderr pipe + stderr, err := cmd.StderrPipe() + if err != nil { + fmt.Fprintf(os.Stderr, "[%s] Error creating stderr pipe: %v\n", name, err) + return + } + + // Start the command + if err := cmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "[%s] Error starting command: %v\n", name, err) + time.Sleep(time.Second) + continue + } + + // Copy output in separate goroutines + var ioWg sync.WaitGroup + ioWg.Add(2) + + go func() { + defer ioWg.Done() + io.Copy(os.Stdout, stdout) + }() + + go func() { + defer ioWg.Done() + io.Copy(os.Stderr, stderr) + }() + + // Wait for command to finish + err = cmd.Wait() + ioWg.Wait() // Ensure all output is copied + + // Check if we should restart + select { + case <-ctx.Done(): + fmt.Printf("[%s] Stopped\n", name) + return + default: + if err != nil { + fmt.Fprintf(os.Stderr, "[%s] Process exited with error: %v\n", name, err) + } else { + fmt.Printf("[%s] Process exited normally\n", name) + } + fmt.Printf("[%s] Restarting in 1 second...\n", name) + time.Sleep(time.Second) + } + } + } +} diff --git a/monitor/filechange.go b/monitor/filechange.go new file mode 100644 index 0000000..71f788f --- /dev/null +++ b/monitor/filechange.go @@ -0,0 +1,6 @@ +package main + +type FileChange struct { + Path string + Operation string +} diff --git a/monitor/go.mod b/monitor/go.mod new file mode 100644 index 0000000..10944cd --- /dev/null +++ b/monitor/go.mod @@ -0,0 +1,8 @@ +module philologue.net/diachron/monitor + +go 1.23.3 + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + golang.org/x/sys v0.13.0 // indirect +) diff --git a/monitor/go.sum b/monitor/go.sum new file mode 100644 index 0000000..c1e3272 --- /dev/null +++ b/monitor/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/monitor/main.go b/monitor/main.go new file mode 100644 index 0000000..5c09175 --- /dev/null +++ b/monitor/main.go @@ -0,0 +1,50 @@ +package main + +import ( + // "context" + "fmt" + "os" + "os/signal" + // "sync" + "syscall" +) + +func main() { + // var program1 = os.Getenv("BUILD_COMMAND") + //var program2 = os.Getenv("RUN_COMMAND") + + var watchedDir = os.Getenv("WATCHED_DIR") + + // Create context for graceful shutdown + // ctx, cancel := context.WithCancel(context.Background()) + //defer cancel() + + // Setup signal handling + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + + fileChanges := make(chan FileChange, 10) + + go watchFiles(watchedDir, fileChanges) + + go printChanges(fileChanges) + + // WaitGroup to track both processes + // var wg sync.WaitGroup + + // Start both processes + //wg.Add(2) + // go runProcess(ctx, &wg, "builder", program1) + // go runProcess(ctx, &wg, "runner", program2) + + // Wait for interrupt signal + <-sigCh + fmt.Println("\nReceived interrupt signal, shutting down...") + + // Cancel context to signal goroutines to stop + /// cancel() + + // Wait for both processes to finish + // wg.Wait() + fmt.Println("All processes terminated cleanly") +} diff --git a/monitor/printchanges.go b/monitor/printchanges.go new file mode 100644 index 0000000..dcb0ad9 --- /dev/null +++ b/monitor/printchanges.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" +) + +func printChanges(changes <-chan FileChange) { + for change := range changes { + fmt.Printf("[%s] %s\n", change.Operation, change.Path) + } +} diff --git a/monitor/watchfiles.go b/monitor/watchfiles.go new file mode 100644 index 0000000..b339def --- /dev/null +++ b/monitor/watchfiles.go @@ -0,0 +1,74 @@ +package main + +import ( + "github.com/fsnotify/fsnotify" + "log" + "os" + "path/filepath" +) + +func watchFiles(dir string, changes chan<- FileChange) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + // Add all directories recursively + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + err = watcher.Add(path) + if err != nil { + log.Printf("Error watching %s: %v\n", path, err) + } + } + return nil + }) + if err != nil { + log.Fatal(err) + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + // Handle different types of events + var operation string + switch { + case event.Op&fsnotify.Write == fsnotify.Write: + operation = "MODIFIED" + case event.Op&fsnotify.Create == fsnotify.Create: + operation = "CREATED" + // If a new directory is created, start watching it + if info, err := os.Stat(event.Name); err == nil && info.IsDir() { + watcher.Add(event.Name) + } + case event.Op&fsnotify.Remove == fsnotify.Remove: + operation = "REMOVED" + case event.Op&fsnotify.Rename == fsnotify.Rename: + operation = "RENAMED" + case event.Op&fsnotify.Chmod == fsnotify.Chmod: + operation = "CHMOD" + default: + operation = "UNKNOWN" + } + + changes <- FileChange{ + Path: event.Name, + Operation: operation, + } + + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Printf("Watcher error: %v\n", err) + } + } +}