package main import ( "crypto/sha256" "fmt" "io" "log" "maps" "os" "os/exec" "os/signal" "runtime" "slices" "time" color "github.com/fatih/color" "github.com/fsnotify/fsnotify" ) var triggered bool = false type Listen struct { FileMap map[string]bool CksumMap map[string][]byte IntervalMap map[string]time.Time Condition string NoCksum bool Interval time.Duration Command []string Quiet bool RunFirst bool } func printStatus(msg string) { if runtime.GOOS == "windows" { fmt.Fprintf(color.Output, "%s", color.HiBlackString(msg)) } else { c := color.New(color.FgHiBlack) c.Fprintf(os.Stderr, "%s", msg) } } 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 { triggered = true if !l.Quiet && !r { if l.Condition == "any" { printStatus(fmt.Sprintf("& File %s modified. ", f)) } else { printStatus("& All files have been modified. ") } } if len(l.Command) >= 1 { if !l.Quiet { printStatus("Running command...\n") } 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) } exit := make(chan bool) s := make(chan os.Signal, 1) signal.Notify(s, os.Interrupt) go func() { if err := cmd.Wait(); err != nil { if rc, ok := err.(*exec.ExitError); ok { if !l.Quiet { switch rc.ExitCode() { case -1: color.New(color.FgYellow).Fprintf(os.Stderr, "\n& WARNING: Command interrupted with ^C.\n") default: color.New(color.FgYellow).Fprintf(os.Stderr, "& WARNING: Command exited with code %d.\n", rc.ExitCode()) } } } } exit <- true }() select { case <-s: cmd.Process.Signal(os.Interrupt) <-exit case <-exit: } if !l.Quiet { printStatus("& Returned to listening...\n") } } else { if !l.Quiet { fmt.Println() } quit <- true triggered = false return true } triggered = false 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.NoCksum { 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 } } else { // return value indicates a break is needed if cksumCheck(l, key, &trigger) { fileMod = key break } } } } // 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.NoCksum { 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 } } else { // return value indicates a break is needed if cksumCheck(l, key, &trigger) { fileMod = key break } } } // 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) { printStatus(fmt.Sprintf("& listen %s\n", VERSION)) printStatus("& This program is free software, and comes with ABSOLUTELY NO WARRANTY.\n") printStatus("&\n") if len(l.Command) >= 1 { printStatus("& This command will run:\n") printStatus("& ") for _, value := range l.Command { printStatus(value + " ") } fmt.Println() printStatus("& When ") } else { printStatus("& listen will exit when ") } if !l.NoCksum { printStatus("the checksum of ") } if len(l.FileMap) >= 2 { if l.Condition == "any" { printStatus("any of ") } else { printStatus("all ") } printStatus("these files have ") } else { printStatus("this file has ") } if l.NoCksum { printStatus("been modified:\n") } else { printStatus("changed:\n") } for key := range l.FileMap { printStatus(fmt.Sprintf("& %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 { printStatus("& -r option specified. ") } runTrigger(l, "", true, quit) } else { printStatus("& Starting now...\n") } // start main loop if l.Interval == time.Duration(0*time.Second) { go loopFsnotify(l, quit) } else { go loopInterval(l, quit) } select { case <-c: for { if !triggered { fmt.Println() break } <-c } case <-quit: } if !l.Quiet { printStatus("& Exiting...\n") } }