package main import ( "crypto/sha256" "fmt" "io" "log" "maps" "os" "os/exec" "os/signal" "slices" "time" color "github.com/fatih/color" "github.com/fsnotify/fsnotify" ) type Listen struct { FileMap map[string]bool CksumMap map[string][]byte IntervalMap map[string]time.Time Condition string Cksum bool Interval time.Duration Command []string Quiet bool RunFirst bool } func cksumCheck(l Listen, k string, t *bool) bool { hasher := sha256.New() f, err := os.Open(k) if err != nil { log.Fatal(err) } defer f.Close() if _, err := io.Copy(hasher, f); err != nil { log.Fatal(err) } if !slices.Equal(l.CksumMap[k], hasher.Sum(nil)) { l.CksumMap[k] = hasher.Sum(nil) if l.Condition == "any" { *t = true return true } l.FileMap[k] = true } return false } func allCheck(l Listen) bool { for value := range maps.Values(l.FileMap) { if !value { return false } } return true } func runTrigger(l Listen, f string, r bool, quit chan bool) bool { if !l.Quiet && !r { if l.Condition == "any" { color.New(color.FgWhite).Fprintf(os.Stderr, "& File %s modified. ", f) } else { color.New(color.FgWhite).Fprint(os.Stderr, "& All files have been modified. ") } } if len(l.Command) >= 1 { if !l.Quiet { color.New(color.FgWhite).Fprintln(os.Stderr, "Running command...") } cmd := exec.Command(l.Command[0], l.Command[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { log.Fatalln(err) } if err := cmd.Wait(); err != nil { if rc, ok := err.(*exec.ExitError); ok { if !l.Quiet { color.New(color.FgYellow).Fprintf(os.Stderr, "& WARNING: Command exited with code %d.\n", rc.ExitCode()) } } } if !l.Quiet { color.New(color.FgWhite).Fprintln(os.Stderr, "& Returned to listening...") } } else { if !l.Quiet { fmt.Println() } quit <- true return true } return false } func loopFsnotify(l Listen, quit chan bool) { watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() for key := range l.FileMap { watcher.Add(key) } for { var renameAdd []string = []string{} fileMod := "" trigger := false select { case event, ok := <-watcher.Events: if !ok { return } for key := range l.FileMap { if event.Name == key { if l.Cksum { // return value indicates a break is needed if cksumCheck(l, key, &trigger) { fileMod = key break } } else { if event.Has(fsnotify.Write) { if l.Condition == "any" { fileMod = key trigger = true break } l.FileMap[key] = true } else if event.Has(fsnotify.Rename) { // we need to rewatch file // sleeping small amount to allow CREATE event to propogate time.Sleep(10 * time.Millisecond) // ... then adding to a list to allow additional time renameAdd = append(renameAdd, key) if l.Condition == "any" { fileMod = key trigger = true break } l.FileMap[key] = true } } } } // end for if l.Condition == "all" { trigger = allCheck(l) } // end case event case _, ok := <-watcher.Errors: if !ok { return } } // end switch // end case errors if trigger { if runTrigger(l, fileMod, false, quit) { return } } // end if trigger for _, value := range renameAdd { err := watcher.Add(value) if err != nil { fmt.Println(err) } } } // end for } func loopInterval(l Listen, quit chan bool) { for { time.Sleep(l.Interval) fileMod := "" trigger := false for key := range l.FileMap { if l.Cksum { // return value indicates a break is needed if cksumCheck(l, key, &trigger) { fileMod = key break } } else { s, err := os.Stat(key) if err != nil { log.Fatal() } if l.IntervalMap[key] != s.ModTime() { l.IntervalMap[key] = s.ModTime() if l.Condition == "any" { fileMod = key trigger = true break } l.FileMap[key] = true } } } // end for key if l.Condition == "all" { trigger = allCheck(l) } if trigger { if runTrigger(l, fileMod, false, quit) { return } } // end if trigger } } func startMessage(l Listen) { c := color.New(color.FgWhite) c.Fprintf(os.Stderr, "& listen %s\n", VERSION) c.Fprintln(os.Stderr, "& This program is free software, and comes with ABSOLUTELY NO WARRANTY.") c.Fprintln(os.Stderr, "& Run 'listen --license' for details.") c.Fprintln(os.Stderr, "&") if len(l.Command) >= 1 { c.Fprintln(os.Stderr, "& This command will run:") c.Fprint(os.Stderr, "& ") for _, value := range l.Command { c.Fprint(os.Stderr, value, " ") } c.Fprintln(os.Stderr) c.Fprint(os.Stderr, "& When ") } else { c.Fprint(os.Stderr, "& listen will exit when ") } if l.Cksum { c.Fprint(os.Stderr, "the checksum of ") } if len(l.FileMap) >= 2 { if l.Condition == "any" { c.Fprint(os.Stderr, "any of ") } else { c.Fprint(os.Stderr, "all ") } c.Fprint(os.Stderr, "these files have ") } else { c.Fprint(os.Stderr, "this file has ") } if l.Cksum { c.Fprintln(os.Stderr, "changed:") } else { c.Fprintln(os.Stderr, "been modified:") } for key := range l.FileMap { c.Fprintf(os.Stderr, "& %s\n", key) } } func (l Listen) Run() { // catch ^C quit := make(chan bool) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) if !l.Quiet { startMessage(l) } if l.RunFirst { if !l.Quiet { color.New(color.FgWhite).Fprintf(os.Stderr, "& -r option specified. ") } runTrigger(l, "", true, quit) } else { color.New(color.FgWhite).Fprintln(os.Stderr, "& Starting now...") } // start main loop if l.Interval == time.Duration(0*time.Second) { go loopFsnotify(l, quit) } else { go loopInterval(l, quit) } select { case <-c: fmt.Println() case <-quit: } if !l.Quiet { color.New(color.FgWhite).Fprintln(os.Stderr, "& Exiting...") } }