github.com/tilt-dev/tilt@v0.36.0/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 continue 156 } 157 158 // Don't send events for directories when the modtime is being changed. 159 // 160 // This is a bit of a hack because every OS represents modtime updates 161 // a bit differently and they don't map well to fsnotify events. 162 // 163 // On Windows, updating the modtime of a directory is a fsnotify.Write. 164 // On Linux, it's a fsnotify.Chmod. 165 isDirUpdateOnly := (e.Op == fsnotify.Write || e.Op == fsnotify.Chmod) && 166 ospath.IsDir(e.Name) 167 if isDirUpdateOnly { 168 continue 169 } 170 171 d.wrappedEvents <- FileEvent{e.Name} 172 continue 173 } 174 175 if d.isWatcherRecursive { 176 if !d.shouldNotify(e.Name) { 177 continue 178 } 179 d.wrappedEvents <- FileEvent{e.Name} 180 continue 181 } 182 183 // If the watcher is not recursive, we have to walk the tree 184 // and add watches manually. We fire the event while we're walking the tree. 185 // because it's a bit more elegant that way. 186 // 187 // TODO(dbentley): if there's a delete should we call d.watcher.Remove to prevent leaking? 188 err := filepath.WalkDir(e.Name, func(path string, info fs.DirEntry, err error) error { 189 if err != nil { 190 return err 191 } 192 193 if d.shouldNotify(path) { 194 d.wrappedEvents <- FileEvent{path} 195 } 196 197 // TODO(dmiller): symlinks 😠198 199 shouldWatch := false 200 if info.IsDir() { 201 // watch directories unless we can skip them entirely 202 shouldSkipDir, err := d.shouldSkipDir(path) 203 if err != nil { 204 return err 205 } 206 if shouldSkipDir { 207 return filepath.SkipDir 208 } 209 210 shouldWatch = true 211 } else { 212 // watch files that are explicitly named, but don't watch others 213 _, ok := d.notifyList[path] 214 if ok { 215 shouldWatch = true 216 } 217 } 218 if shouldWatch { 219 err := d.add(path) 220 if err != nil && !os.IsNotExist(err) { 221 d.log.Infof("Error watching path %s: %s", e.Name, err) 222 } 223 } 224 return nil 225 }) 226 if err != nil && !os.IsNotExist(err) { 227 d.log.Infof("Error walking directory %s: %s", e.Name, err) 228 } 229 } 230 } 231 232 func (d *naiveNotify) shouldNotify(path string) bool { 233 ignore, err := d.ignore.Matches(path) 234 if err != nil { 235 d.log.Infof("Error matching path %q: %v", path, err) 236 } else if ignore { 237 return false 238 } 239 240 if _, ok := d.notifyList[path]; ok { 241 // We generally don't care when directories change at the root of an ADD 242 isDir := ospath.IsDirLstat(path) 243 if isDir { 244 return false 245 } 246 return true 247 } 248 249 for root := range d.notifyList { 250 if ospath.IsChild(root, path) { 251 return true 252 } 253 } 254 return false 255 } 256 257 func (d *naiveNotify) shouldSkipDir(path string) (bool, error) { 258 // If path is directly in the notifyList, we should always watch it. 259 if d.notifyList[path] { 260 return false, nil 261 } 262 263 skip, err := d.ignore.MatchesEntireDir(path) 264 if err != nil { 265 return false, errors.Wrap(err, "shouldSkipDir") 266 } 267 268 if skip { 269 return true, nil 270 } 271 272 // Suppose we're watching 273 // /src/.tiltignore 274 // but the .tiltignore file doesn't exist. 275 // 276 // Our watcher will create an inotify watch on /src/. 277 // 278 // But then we want to make sure we don't recurse from /src/ down to /src/node_modules. 279 // 280 // To handle this case, we only want to traverse dirs that are: 281 // - A child of a directory that's in our notify list, or 282 // - A parent of a directory that's in our notify list 283 // (i.e., to cover the "path doesn't exist" case). 284 for root := range d.notifyList { 285 if ospath.IsChild(root, path) || ospath.IsChild(path, root) { 286 return false, nil 287 } 288 } 289 return true, nil 290 } 291 292 func (d *naiveNotify) add(path string) error { 293 err := d.watcher.Add(path) 294 if err != nil { 295 return err 296 } 297 d.numWatches++ 298 numberOfWatches.Add(1) 299 return nil 300 } 301 302 func newWatcher(paths []string, ignore PathMatcher, l logger.Logger) (*naiveNotify, error) { 303 if ignore == nil { 304 return nil, fmt.Errorf("newWatcher: ignore is nil") 305 } 306 307 fsw, err := fsnotify.NewWatcher() 308 if err != nil { 309 if strings.Contains(err.Error(), "too many open files") && runtime.GOOS == "linux" { 310 return nil, fmt.Errorf("Hit OS limits creating a watcher.\n" + 311 "Run 'sysctl fs.inotify.max_user_instances' to check your inotify limits.\n" + 312 "To raise them, run 'sudo sysctl fs.inotify.max_user_instances=1024'") 313 } 314 return nil, errors.Wrap(err, "creating file watcher") 315 } 316 MaybeIncreaseBufferSize(fsw) 317 318 err = fsw.SetRecursive() 319 isWatcherRecursive := err == nil 320 321 wrappedEvents := make(chan FileEvent) 322 notifyList := make(map[string]bool, len(paths)) 323 if isWatcherRecursive { 324 paths = dedupePathsForRecursiveWatcher(paths) 325 } 326 for _, path := range paths { 327 path, err := filepath.Abs(path) 328 if err != nil { 329 return nil, errors.Wrap(err, "newWatcher") 330 } 331 notifyList[path] = true 332 } 333 334 wmw := &naiveNotify{ 335 notifyList: notifyList, 336 ignore: ignore, 337 log: l, 338 watcher: fsw, 339 events: fsw.Events, 340 wrappedEvents: wrappedEvents, 341 errors: fsw.Errors, 342 isWatcherRecursive: isWatcherRecursive, 343 } 344 345 return wmw, nil 346 } 347 348 var _ Notify = &naiveNotify{} 349 350 func greatestExistingAncestors(paths []string) ([]string, error) { 351 result := []string{} 352 for _, p := range paths { 353 newP, err := greatestExistingAncestor(p) 354 if err != nil { 355 return nil, fmt.Errorf("Finding ancestor of %s: %v", p, err) 356 } 357 result = append(result, newP) 358 } 359 return result, nil 360 }