listen/listen.go

330 lines
5.8 KiB
Go
Raw Normal View History

package main
import (
"crypto/sha256"
"fmt"
"io"
"log"
"maps"
"os"
"os/exec"
"os/signal"
2025-06-29 15:05:18 -06:00
"runtime"
"slices"
"time"
2025-06-29 12:58:37 -06:00
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
2025-06-29 12:58:37 -06:00
RunFirst bool
}
2025-06-29 15:05:18 -06:00
func printStatus(msg string) {
c := color.New(color.FgWhite)
if runtime.GOOS == "windows" {
c.Fprintf(color.Output, "%s", msg)
} else {
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
}
2025-06-29 12:58:37 -06:00
func runTrigger(l Listen, f string, r bool, quit chan bool) bool {
if !l.Quiet && !r {
if l.Condition == "any" {
2025-06-29 15:05:18 -06:00
printStatus(fmt.Sprintf("& File %s modified. ", f))
2025-06-29 12:58:37 -06:00
} else {
2025-06-29 15:05:18 -06:00
printStatus("& All files have been modified. ")
2025-06-29 12:58:37 -06:00
}
}
if len(l.Command) >= 1 {
if !l.Quiet {
2025-06-29 15:05:18 -06:00
printStatus("Running command...\n")
2025-06-29 12:58:37 -06:00
}
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 {
2025-06-29 15:05:18 -06:00
printStatus("& Returned to listening...\n")
2025-06-29 12:58:37 -06:00
}
} 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{}
2025-06-29 12:58:37 -06:00
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) {
2025-06-29 12:58:37 -06:00
fileMod = key
break
}
} else {
if event.Has(fsnotify.Write) {
if l.Condition == "any" {
2025-06-29 12:58:37 -06:00
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" {
2025-06-29 12:58:37 -06:00
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 {
2025-06-29 12:58:37 -06:00
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)
2025-06-29 12:58:37 -06:00
fileMod := ""
trigger := false
for key := range l.FileMap {
if l.Cksum {
// return value indicates a break is needed
if cksumCheck(l, key, &trigger) {
2025-06-29 12:58:37 -06:00
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" {
2025-06-29 12:58:37 -06:00
fileMod = key
trigger = true
break
}
l.FileMap[key] = true
}
}
} // end for key
if l.Condition == "all" {
trigger = allCheck(l)
}
if trigger {
2025-06-29 12:58:37 -06:00
if runTrigger(l, fileMod, false, quit) {
return
}
} // end if trigger
}
}
2025-06-29 12:58:37 -06:00
func startMessage(l Listen) {
2025-06-29 15:05:18 -06:00
printStatus(fmt.Sprintf("& listen %s\n", VERSION))
printStatus("& This program is free software, and comes with ABSOLUTELY NO WARRANTY.\n")
printStatus("& Run 'listen --license' for details.\n")
printStatus("&\n")
2025-06-29 12:58:37 -06:00
if len(l.Command) >= 1 {
2025-06-29 15:05:18 -06:00
printStatus("& This command will run:\n")
printStatus("& ")
2025-06-29 12:58:37 -06:00
for _, value := range l.Command {
2025-06-29 15:05:18 -06:00
printStatus(value + " ")
2025-06-29 12:58:37 -06:00
}
2025-06-29 15:05:18 -06:00
fmt.Println()
printStatus("& When ")
2025-06-29 12:58:37 -06:00
} else {
2025-06-29 15:05:18 -06:00
printStatus("& listen will exit when ")
2025-06-29 12:58:37 -06:00
}
if l.Cksum {
2025-06-29 15:05:18 -06:00
printStatus("the checksum of ")
2025-06-29 12:58:37 -06:00
}
if len(l.FileMap) >= 2 {
if l.Condition == "any" {
2025-06-29 15:05:18 -06:00
printStatus("any of ")
2025-06-29 12:58:37 -06:00
} else {
2025-06-29 15:05:18 -06:00
printStatus("all ")
2025-06-29 12:58:37 -06:00
}
2025-06-29 15:05:18 -06:00
printStatus("these files have ")
2025-06-29 12:58:37 -06:00
} else {
2025-06-29 15:05:18 -06:00
printStatus("this file has ")
2025-06-29 12:58:37 -06:00
}
if l.Cksum {
2025-06-29 15:05:18 -06:00
printStatus("changed:\n")
2025-06-29 12:58:37 -06:00
} else {
2025-06-29 15:05:18 -06:00
printStatus("been modified:\n")
2025-06-29 12:58:37 -06:00
}
for key := range l.FileMap {
2025-06-29 15:05:18 -06:00
printStatus(fmt.Sprintf("& %s\n", key))
2025-06-29 12:58:37 -06:00
}
}
func (l Listen) Run() {
// catch ^C
quit := make(chan bool)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
2025-06-29 12:58:37 -06:00
if !l.Quiet {
startMessage(l)
}
if l.RunFirst {
if !l.Quiet {
2025-06-29 15:05:18 -06:00
printStatus("& -r option specified. ")
2025-06-29 12:58:37 -06:00
}
runTrigger(l, "", true, quit)
} else {
2025-06-29 15:05:18 -06:00
printStatus("& Starting now...\n")
2025-06-29 12:58:37 -06:00
}
// start main loop
if l.Interval == time.Duration(0*time.Second) {
go loopFsnotify(l, quit)
} else {
go loopInterval(l, quit)
}
select {
case <-c:
2025-06-29 12:58:37 -06:00
fmt.Println()
case <-quit:
}
2025-06-29 12:58:37 -06:00
if !l.Quiet {
2025-06-29 15:05:18 -06:00
printStatus("& Exiting...\n")
2025-06-29 12:58:37 -06:00
}
}