github.com/please-build/puku@v1.7.3-0.20240516143641-f7d7f4941f57/watch/watch.go (about) 1 package watch 2 3 import ( 4 "os" 5 "path/filepath" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/fsnotify/fsnotify" 11 12 "github.com/please-build/puku/generate" 13 "github.com/please-build/puku/logging" 14 "github.com/please-build/puku/please" 15 ) 16 17 var log = logging.GetLogger() 18 19 const debounceDuration = 200 * time.Millisecond 20 21 // debouncer batches up updates to paths, waiting for a debounceDuration to pass. This avoids running puku many times 22 // during git checkouts etc. but it also avoids inconsistent state when files are being moved around rapidly. 23 type debouncer struct { 24 paths map[string]struct{} 25 timer *time.Timer 26 mux sync.Mutex 27 config *please.Config 28 } 29 30 // updatePath adds a path to the batch and resets the timer to the deboundDuration 31 func (d *debouncer) updatePath(path string) { 32 d.mux.Lock() 33 defer d.mux.Unlock() 34 35 d.paths[path] = struct{}{} 36 if d.timer != nil { 37 d.timer.Stop() 38 d.timer.Reset(debounceDuration) 39 } else { 40 d.timer = time.NewTimer(debounceDuration) 41 go d.wait() 42 } 43 } 44 45 // wait waits for the timer to fire before updating the paths 46 func (d *debouncer) wait() { 47 <-d.timer.C 48 49 d.mux.Lock() 50 51 paths := make([]string, 0, len(d.paths)) 52 for p := range d.paths { 53 paths = append(paths, p) 54 } 55 if err := generate.Update(d.config, paths...); err != nil { 56 log.Warningf("failed to update: %v", err) 57 } else { 58 log.Infof("Updated paths: %v ", strings.Join(paths, ", ")) 59 } 60 d.paths = map[string]struct{}{} 61 d.mux.Unlock() 62 63 //nolint:staticcheck 64 d.wait() // infinite recursive calls are a lint error but it's what we want here 65 } 66 67 func Watch(config *please.Config, paths ...string) error { 68 if len(paths) < 1 { 69 return nil 70 } 71 watcher, err := fsnotify.NewWatcher() 72 if err != nil { 73 return err 74 } 75 defer watcher.Close() 76 77 d := &debouncer{ 78 paths: map[string]struct{}{}, 79 config: config, 80 } 81 82 go func() { 83 for { 84 select { 85 case event, ok := <-watcher.Events: 86 if !ok { 87 return 88 } 89 if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) && !event.Has(fsnotify.Remove) { 90 break 91 } 92 93 if filepath.Ext(event.Name) == ".go" { 94 d.updatePath(filepath.Dir(event.Name)) 95 break 96 } 97 98 if event.Has(fsnotify.Create) { 99 if info, err := os.Lstat(event.Name); err == nil { 100 if info.IsDir() { 101 if err := add(watcher, event.Name); err != nil { 102 log.Warningf("failed to set up watcher: %v", err) 103 } 104 } 105 } 106 } 107 case err, ok := <-watcher.Errors: 108 if !ok { 109 return 110 } 111 log.Warningf("watcher error: %s", err) 112 } 113 } 114 }() 115 116 if err := add(watcher, paths...); err != nil { 117 return err 118 } 119 log.Info("And so my watch begins...") 120 select {} 121 } 122 123 func add(watcher *fsnotify.Watcher, paths ...string) error { 124 for _, path := range paths { 125 if path == "" { 126 path = "." 127 } 128 err := watcher.Add(path) 129 if err != nil { 130 return err 131 } 132 } 133 return nil 134 }