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  }