github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/watch/watcher_naive.go (about) 1 //go:build !darwin 2 // +build !darwin 3 4 package watch 5 6 import ( 7 "fmt" 8 "io/fs" 9 "os" 10 "path/filepath" 11 "runtime" 12 "strings" 13 14 "github.com/pkg/errors" 15 16 "github.com/tilt-dev/fsnotify" 17 "github.com/tilt-dev/tilt/internal/ospath" 18 "github.com/tilt-dev/tilt/pkg/logger" 19 ) 20 21 // A naive file watcher that uses the plain fsnotify API. 22 // Used on all non-Darwin systems (including Windows & Linux). 23 // 24 // All OS-specific codepaths are handled by fsnotify. 25 type naiveNotify struct { 26 // Paths that we're watching that should be passed up to the caller. 27 // Note that we may have to watch ancestors of these paths 28 // in order to fulfill the API promise. 29 // 30 // We often need to check if paths are a child of a path in 31 // the notify list. It might be better to store this in a tree 32 // structure, so we can filter the list quickly. 33 notifyList map[string]bool 34 35 ignore PathMatcher 36 log logger.Logger 37 38 isWatcherRecursive bool 39 watcher *fsnotify.Watcher 40 events chan fsnotify.Event 41 wrappedEvents chan FileEvent 42 errors chan error 43 numWatches int64 44 } 45 46 func (d *naiveNotify) Start() error { 47 if len(d.notifyList) == 0 { 48 return nil 49 } 50 51 pathsToWatch := []string{} 52 for path := range d.notifyList { 53 pathsToWatch = append(pathsToWatch, path) 54 } 55 56 pathsToWatch, err := greatestExistingAncestors(pathsToWatch) 57 if err != nil { 58 return err 59 } 60 if d.isWatcherRecursive { 61 pathsToWatch = dedupePathsForRecursiveWatcher(pathsToWatch) 62 } 63 64 for _, name := range pathsToWatch { 65 fi, err := os.Stat(name) 66 if err != nil && !os.IsNotExist(err) { 67 return errors.Wrapf(err, "notify.Add(%q)", name) 68 } 69 70 // if it's a file that doesn't exist, 71 // we should have caught that above, let's just skip it. 72 if os.IsNotExist(err) { 73 continue 74 } else if fi.IsDir() { 75 err = d.watchRecursively(name) 76 if err != nil { 77 return errors.Wrapf(err, "notify.Add(%q)", name) 78 } 79 } else { 80 err = d.add(filepath.Dir(name)) 81 if err != nil { 82 return errors.Wrapf(err, "notify.Add(%q)", filepath.Dir(name)) 83 } 84 } 85 } 86 87 go d.loop() 88 89 return nil 90 } 91 92 func (d *naiveNotify) watchRecursively(dir string) error { 93 if d.isWatcherRecursive { 94 err := d.add(dir) 95 if err == nil || os.IsNotExist(err) { 96 return nil 97 } 98 return errors.Wrapf(err, "watcher.Add(%q)", dir) 99 } 100 101 return filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error { 102 if err != nil { 103 return err 104 } 105 106 if !info.IsDir() { 107 return nil 108 } 109 110 shouldSkipDir, err := d.shouldSkipDir(path) 111 if err != nil { 112 return err 113 } 114 115 if shouldSkipDir { 116 return filepath.SkipDir 117 } 118 119 err = d.add(path) 120 if err != nil { 121 if os.IsNotExist(err) { 122 return nil 123 } 124 return errors.Wrapf(err, "watcher.Add(%q)", path) 125 } 126 return nil 127 }) 128 } 129 130 func (d *naiveNotify) Close() error { 131 numberOfWatches.Add(-d.numWatches) 132 d.numWatches = 0 133 return d.watcher.Close() 134 } 135 136 func (d *naiveNotify) Events() chan FileEvent { 137 return d.wrappedEvents 138 } 139 140 func (d *naiveNotify) Errors() chan error { 141 return d.errors 142 } 143 144 func (d *naiveNotify) loop() { 145 defer close(d.wrappedEvents) 146 for e := range d.events { 147 // The Windows fsnotify event stream sometimes gets events with empty names 148 // that are also sent to the error stream. Hmmmm... 149 if e.Name == "" { 150 continue 151 } 152 153 if e.Op&fsnotify.Create != fsnotify.Create { 154 if d.shouldNotify(e.Name) { 155 d.wrappedEvents <- FileEvent{e.Name} 156 } 157 continue 158 } 159 160 if d.isWatcherRecursive { 161 if d.shouldNotify(e.Name) { 162 d.wrappedEvents <- FileEvent{e.Name} 163 } 164 continue 165 } 166 167 // If the watcher is not recursive, we have to walk the tree 168 // and add watches manually. We fire the event while we're walking the tree. 169 // because it's a bit more elegant that way. 170 // 171 // TODO(dbentley): if there's a delete should we call d.watcher.Remove to prevent leaking? 172 err := filepath.WalkDir(e.Name, func(path string, info fs.DirEntry, err error) error { 173 if err != nil { 174 return err 175 } 176 177 if d.shouldNotify(path) { 178 d.wrappedEvents <- FileEvent{path} 179 } 180 181 // TODO(dmiller): symlinks 😠182 183 shouldWatch := false 184 if info.IsDir() { 185 // watch directories unless we can skip them entirely 186 shouldSkipDir, err := d.shouldSkipDir(path) 187 if err != nil { 188 return err 189 } 190 if shouldSkipDir { 191 return filepath.SkipDir 192 } 193 194 shouldWatch = true 195 } else { 196 // watch files that are explicitly named, but don't watch others 197 _, ok := d.notifyList[path] 198 if ok { 199 shouldWatch = true 200 } 201 } 202 if shouldWatch { 203 err := d.add(path) 204 if err != nil && !os.IsNotExist(err) { 205 d.log.Infof("Error watching path %s: %s", e.Name, err) 206 } 207 } 208 return nil 209 }) 210 if err != nil && !os.IsNotExist(err) { 211 d.log.Infof("Error walking directory %s: %s", e.Name, err) 212 } 213 } 214 } 215 216 func (d *naiveNotify) shouldNotify(path string) bool { 217 ignore, err := d.ignore.Matches(path) 218 if err != nil { 219 d.log.Infof("Error matching path %q: %v", path, err) 220 } else if ignore { 221 return false 222 } 223 224 if _, ok := d.notifyList[path]; ok { 225 // We generally don't care when directories change at the root of an ADD 226 stat, err := os.Lstat(path) 227 isDir := err == nil && stat.IsDir() 228 if isDir { 229 return false 230 } 231 return true 232 } 233 234 for root := range d.notifyList { 235 if ospath.IsChild(root, path) { 236 return true 237 } 238 } 239 return false 240 } 241 242 func (d *naiveNotify) shouldSkipDir(path string) (bool, error) { 243 // If path is directly in the notifyList, we should always watch it. 244 if d.notifyList[path] { 245 return false, nil 246 } 247 248 skip, err := d.ignore.MatchesEntireDir(path) 249 if err != nil { 250 return false, errors.Wrap(err, "shouldSkipDir") 251 } 252 253 if skip { 254 return true, nil 255 } 256 257 // Suppose we're watching 258 // /src/.tiltignore 259 // but the .tiltignore file doesn't exist. 260 // 261 // Our watcher will create an inotify watch on /src/. 262 // 263 // But then we want to make sure we don't recurse from /src/ down to /src/node_modules. 264 // 265 // To handle this case, we only want to traverse dirs that are: 266 // - A child of a directory that's in our notify list, or 267 // - A parent of a directory that's in our notify list 268 // (i.e., to cover the "path doesn't exist" case). 269 for root := range d.notifyList { 270 if ospath.IsChild(root, path) || ospath.IsChild(path, root) { 271 return false, nil 272 } 273 } 274 return true, nil 275 } 276 277 func (d *naiveNotify) add(path string) error { 278 err := d.watcher.Add(path) 279 if err != nil { 280 return err 281 } 282 d.numWatches++ 283 numberOfWatches.Add(1) 284 return nil 285 } 286 287 func newWatcher(paths []string, ignore PathMatcher, l logger.Logger) (*naiveNotify, error) { 288 if ignore == nil { 289 return nil, fmt.Errorf("newWatcher: ignore is nil") 290 } 291 292 fsw, err := fsnotify.NewWatcher() 293 if err != nil { 294 if strings.Contains(err.Error(), "too many open files") && runtime.GOOS == "linux" { 295 return nil, fmt.Errorf("Hit OS limits creating a watcher.\n" + 296 "Run 'sysctl fs.inotify.max_user_instances' to check your inotify limits.\n" + 297 "To raise them, run 'sudo sysctl fs.inotify.max_user_instances=1024'") 298 } 299 return nil, errors.Wrap(err, "creating file watcher") 300 } 301 MaybeIncreaseBufferSize(fsw) 302 303 err = fsw.SetRecursive() 304 isWatcherRecursive := err == nil 305 306 wrappedEvents := make(chan FileEvent) 307 notifyList := make(map[string]bool, len(paths)) 308 if isWatcherRecursive { 309 paths = dedupePathsForRecursiveWatcher(paths) 310 } 311 for _, path := range paths { 312 path, err := filepath.Abs(path) 313 if err != nil { 314 return nil, errors.Wrap(err, "newWatcher") 315 } 316 notifyList[path] = true 317 } 318 319 wmw := &naiveNotify{ 320 notifyList: notifyList, 321 ignore: ignore, 322 log: l, 323 watcher: fsw, 324 events: fsw.Events, 325 wrappedEvents: wrappedEvents, 326 errors: fsw.Errors, 327 isWatcherRecursive: isWatcherRecursive, 328 } 329 330 return wmw, nil 331 } 332 333 var _ Notify = &naiveNotify{} 334 335 func greatestExistingAncestors(paths []string) ([]string, error) { 336 result := []string{} 337 for _, p := range paths { 338 newP, err := greatestExistingAncestor(p) 339 if err != nil { 340 return nil, fmt.Errorf("Finding ancestor of %s: %v", p, err) 341 } 342 result = append(result, newP) 343 } 344 return result, nil 345 }