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  }