package main import ( "context" "errors" "fmt" "io" "log" "os" "os/signal" "slices" "strings" "sync" "syscall" "time" "github.com/fsnotify/fsnotify" "github.com/jessevdk/go-flags" "alin.ovh/erl/command" "alin.ovh/erl/ignore" "alin.ovh/erl/state" "alin.ovh/erl/watcher" ) type Options struct { Exec string `short:"x" long:"exec" description:"command to execute on file change"` Quiet bool `short:"q" long:"quiet" description:"suppress own output"` Verbose bool `short:"v" long:"verbose" description:"verbose output (print events)"` } func Start(ctx context.Context, verbose *log.Logger, w watcher.Watcher, sm *state.StateMachine) { var wg sync.WaitGroup wg.Add(1) go func(events <-chan watcher.Event, errors <-chan error) { defer wg.Done() sm.SendEvent(state.Start) for { select { case <-ctx.Done(): sm.SendEvent(state.Signal) return case event, ok := <-events: if !ok { return } // skip if _only_ chmod if event.Op == fsnotify.Chmod { continue } verbose.Printf("event: %s %s\n", event.Name, event.Op.String()) if event.Op.Has(fsnotify.Create) { stat, err := os.Stat(event.Name) if err != nil { log.Printf("Error getting file info: %v\n", err) continue } if stat.IsDir() { err = w.AddRecursive(event.Name) if err != nil { log.Printf("Error adding directory to watcher: %v\n", err) } } } if event.Op.Has(fsnotify.Remove) { for _, dir := range w.WatchList() { if strings.HasPrefix(dir, event.Name) { err := w.Remove(dir) if err != nil { log.Printf("Error removing directory from watcher: %v\n", err) } } } } sm.SendEvent(state.Restart) time.Sleep(100 * time.Millisecond) case err, ok := <-errors: if !ok { return } log.Printf("Error: %v\n", err) } } }(w.Monitor()) err := w.AddRecursive(".") if err != nil { log.Fatalf("failed to add directory to watcher: %v", err) } <-ctx.Done() log.Println("shutting down") sm.SendEvent(state.Signal) wg.Wait() } func main() { var opts Options log.SetFlags(log.Lmsgprefix) fp := flags.NewParser(&opts, flags.Default) args, err := fp.Parse() if err != nil { if errors.Is(err, flags.ErrHelp) { os.Exit(0) } log.Fatalf("failed to parse flags: %v", err) } program := opts.Exec if program == "" { program = "go" if len(args) == 0 { args = []string{"run", "."} } else { args = slices.Insert(args, 0, "run") } } wd, err := os.Getwd() if err != nil { log.Fatalf("failed to get working directory: %v", err) } ctx, cancel := signal.NotifyContext( context.Background(), os.Interrupt, syscall.SIGHUP, syscall.SIGTERM, ) defer cancel() filter := ignore.New(wd) err = filter.ReadIgnoreFiles(ctx) if err != nil { panic(fmt.Sprintf("failed to read ignore files: %v", err)) } watcher, err := watcher.New(watcher.Options{ Filter: *filter, }) if err != nil { panic(fmt.Sprintf("failed to create watcher: %v", err)) } defer watcher.Close() copts := command.Options{} if opts.Quiet { copts.Output = io.Discard } var logger *log.Logger if opts.Verbose { logger = log.New(os.Stderr, "", 0) } else { logger = log.New(io.Discard, "", 0) } cmd := command.New(program, args, copts) sm := state.New(cmd, log.New(os.Stderr, "state: ", log.Lmsgprefix)) Start(ctx, logger, watcher, sm) }