github.com/tilt-dev/tilt@v0.36.0/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 isDir := e.Flags&fsevents.ItemIsDir == fsevents.ItemIsDir 42 if isDir && isPathWereWatching { 43 // For consistency with Linux and Windows, don't fire any events 44 // for directories that we're watching -- only their contents. 45 continue 46 } 47 48 // On MacOS, modifying a directory entry fires Created | InodeMetaMod 49 // Ignore these events, mod time modifications shouldnt trigger copies. 50 if isDir && (e.Flags&fsevents.ItemInodeMetaMod) == fsevents.ItemInodeMetaMod { 51 continue 52 } 53 54 ignore, err := d.ignore.Matches(e.Path) 55 if err != nil { 56 d.logger.Infof("Error matching path %q: %v", e.Path, err) 57 } else if ignore { 58 continue 59 } 60 61 d.events <- NewFileEvent(e.Path) 62 } 63 } 64 } 65 } 66 67 // Add a path to be watched. Should only be called during initialization. 68 func (d *darwinNotify) initAdd(name string) { 69 d.stream.Paths = append(d.stream.Paths, name) 70 71 if d.pathsWereWatching == nil { 72 d.pathsWereWatching = make(map[string]interface{}) 73 } 74 d.pathsWereWatching[name] = struct{}{} 75 } 76 77 func (d *darwinNotify) Start() error { 78 if len(d.stream.Paths) == 0 { 79 return nil 80 } 81 82 numberOfWatches.Add(int64(len(d.stream.Paths))) 83 84 if err := d.stream.Start(); err != nil { 85 return err 86 } 87 88 go d.loop() 89 90 return nil 91 } 92 93 func (d *darwinNotify) Close() error { 94 numberOfWatches.Add(int64(-len(d.stream.Paths))) 95 96 d.stream.Stop() 97 close(d.errors) 98 close(d.stop) 99 100 return nil 101 } 102 103 func (d *darwinNotify) Events() chan FileEvent { 104 return d.events 105 } 106 107 func (d *darwinNotify) Errors() chan error { 108 return d.errors 109 } 110 111 func newWatcher(paths []string, ignore PathMatcher, l logger.Logger) (*darwinNotify, error) { 112 dw := &darwinNotify{ 113 ignore: ignore, 114 logger: l, 115 stream: &fsevents.EventStream{ 116 Latency: 1 * time.Millisecond, 117 Flags: fsevents.FileEvents, 118 // NOTE(dmiller): this corresponds to the `sinceWhen` parameter in FSEventStreamCreate 119 // https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate 120 EventID: fsevents.LatestEventID(), 121 }, 122 events: make(chan FileEvent), 123 errors: make(chan error), 124 stop: make(chan struct{}), 125 } 126 127 paths = dedupePathsForRecursiveWatcher(paths) 128 for _, path := range paths { 129 path, err := filepath.Abs(path) 130 if err != nil { 131 return nil, errors.Wrap(err, "newWatcher") 132 } 133 dw.initAdd(path) 134 } 135 136 return dw, nil 137 } 138 139 var _ Notify = &darwinNotify{}