github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/watch/watcher_darwin.go (about)

     1  package watch
     2  
     3  import (
     4  	"path/filepath"
     5  	"time"
     6  
     7  	"github.com/pkg/errors"
     8  
     9  	"github.com/tilt-dev/tilt/pkg/logger"
    10  
    11  	"github.com/fsnotify/fsevents"
    12  )
    13  
    14  // A file watcher optimized for Darwin.
    15  // Uses FSEvents to avoid the terrible perf characteristics of kqueue.
    16  type darwinNotify struct {
    17  	stream *fsevents.EventStream
    18  	events chan FileEvent
    19  	errors chan error
    20  	stop   chan struct{}
    21  
    22  	pathsWereWatching map[string]interface{}
    23  	ignore            PathMatcher
    24  	logger            logger.Logger
    25  }
    26  
    27  func (d *darwinNotify) loop() {
    28  	for {
    29  		select {
    30  		case <-d.stop:
    31  			return
    32  		case events, ok := <-d.stream.Events:
    33  			if !ok {
    34  				return
    35  			}
    36  
    37  			for _, e := range events {
    38  				e.Path = filepath.Join("/", e.Path)
    39  
    40  				_, isPathWereWatching := d.pathsWereWatching[e.Path]
    41  				if e.Flags&fsevents.ItemIsDir == fsevents.ItemIsDir && isPathWereWatching {
    42  					// For consistency with Linux and Windows, don't fire any events
    43  					// for directories that we're watching -- only their contents.
    44  					continue
    45  				}
    46  
    47  				ignore, err := d.ignore.Matches(e.Path)
    48  				if err != nil {
    49  					d.logger.Infof("Error matching path %q: %v", e.Path, err)
    50  				} else if ignore {
    51  					continue
    52  				}
    53  
    54  				d.events <- NewFileEvent(e.Path)
    55  			}
    56  		}
    57  	}
    58  }
    59  
    60  // Add a path to be watched. Should only be called during initialization.
    61  func (d *darwinNotify) initAdd(name string) {
    62  	d.stream.Paths = append(d.stream.Paths, name)
    63  
    64  	if d.pathsWereWatching == nil {
    65  		d.pathsWereWatching = make(map[string]interface{})
    66  	}
    67  	d.pathsWereWatching[name] = struct{}{}
    68  }
    69  
    70  func (d *darwinNotify) Start() error {
    71  	if len(d.stream.Paths) == 0 {
    72  		return nil
    73  	}
    74  
    75  	numberOfWatches.Add(int64(len(d.stream.Paths)))
    76  
    77  	d.stream.Start()
    78  
    79  	go d.loop()
    80  
    81  	return nil
    82  }
    83  
    84  func (d *darwinNotify) Close() error {
    85  	numberOfWatches.Add(int64(-len(d.stream.Paths)))
    86  
    87  	d.stream.Stop()
    88  	close(d.errors)
    89  	close(d.stop)
    90  
    91  	return nil
    92  }
    93  
    94  func (d *darwinNotify) Events() chan FileEvent {
    95  	return d.events
    96  }
    97  
    98  func (d *darwinNotify) Errors() chan error {
    99  	return d.errors
   100  }
   101  
   102  func newWatcher(paths []string, ignore PathMatcher, l logger.Logger) (*darwinNotify, error) {
   103  	dw := &darwinNotify{
   104  		ignore: ignore,
   105  		logger: l,
   106  		stream: &fsevents.EventStream{
   107  			Latency: 1 * time.Millisecond,
   108  			Flags:   fsevents.FileEvents,
   109  			// NOTE(dmiller): this corresponds to the `sinceWhen` parameter in FSEventStreamCreate
   110  			// https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate
   111  			EventID: fsevents.LatestEventID(),
   112  		},
   113  		events: make(chan FileEvent),
   114  		errors: make(chan error),
   115  		stop:   make(chan struct{}),
   116  	}
   117  
   118  	paths = dedupePathsForRecursiveWatcher(paths)
   119  	for _, path := range paths {
   120  		path, err := filepath.Abs(path)
   121  		if err != nil {
   122  			return nil, errors.Wrap(err, "newWatcher")
   123  		}
   124  		dw.initAdd(path)
   125  	}
   126  
   127  	return dw, nil
   128  }
   129  
   130  var _ Notify = &darwinNotify{}