320 lines
5.9 KiB
Go
320 lines
5.9 KiB
Go
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...")
|
|
}
|
|
|
|
}
|