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  }