aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/filewatcher/backend.go
diff options
context:
space:
mode:
Diffstat (limited to 'cli/internal/filewatcher/backend.go')
-rw-r--r--cli/internal/filewatcher/backend.go209
1 files changed, 209 insertions, 0 deletions
diff --git a/cli/internal/filewatcher/backend.go b/cli/internal/filewatcher/backend.go
new file mode 100644
index 0000000..b8b7fa8
--- /dev/null
+++ b/cli/internal/filewatcher/backend.go
@@ -0,0 +1,209 @@
+//go:build !darwin
+// +build !darwin
+
+package filewatcher
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/hashicorp/go-hclog"
+ "github.com/karrick/godirwalk"
+ "github.com/pkg/errors"
+ "github.com/vercel/turbo/cli/internal/doublestar"
+ "github.com/vercel/turbo/cli/internal/fs"
+ "github.com/vercel/turbo/cli/internal/turbopath"
+)
+
+// watchAddMode is used to indicate whether watchRecursively should synthesize events
+// for existing files.
+type watchAddMode int
+
+const (
+ dontSynthesizeEvents watchAddMode = iota
+ synthesizeEvents
+)
+
+type fsNotifyBackend struct {
+ watcher *fsnotify.Watcher
+ events chan Event
+ errors chan error
+ logger hclog.Logger
+
+ mu sync.Mutex
+ allExcludes []string
+ closed bool
+}
+
+func (f *fsNotifyBackend) Events() <-chan Event {
+ return f.events
+}
+
+func (f *fsNotifyBackend) Errors() <-chan error {
+ return f.errors
+}
+
+func (f *fsNotifyBackend) Close() error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ if f.closed {
+ return ErrFilewatchingClosed
+ }
+ f.closed = true
+ close(f.events)
+ close(f.errors)
+ if err := f.watcher.Close(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// onFileAdded helps up paper over cross-platform inconsistencies in fsnotify.
+// Some fsnotify backends automatically add the contents of directories. Some do
+// not. Adding a watch is idempotent, so anytime any file we care about gets added,
+// watch it.
+func (f *fsNotifyBackend) onFileAdded(name turbopath.AbsoluteSystemPath) error {
+ info, err := name.Lstat()
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ // We can race with a file being added and removed. Ignore it
+ return nil
+ }
+ return errors.Wrapf(err, "error checking lstat of new file %v", name)
+ }
+ if info.IsDir() {
+ // If a directory has been added, we need to synthesize events for everything it contains
+ if err := f.watchRecursively(name, []string{}, synthesizeEvents); err != nil {
+ return errors.Wrapf(err, "failed recursive watch of %v", name)
+ }
+ } else {
+ if err := f.watcher.Add(name.ToString()); err != nil {
+ return errors.Wrapf(err, "failed adding watch to %v", name)
+ }
+ }
+ return nil
+}
+
+func (f *fsNotifyBackend) watchRecursively(root turbopath.AbsoluteSystemPath, excludePatterns []string, addMode watchAddMode) error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ err := fs.WalkMode(root.ToString(), func(name string, isDir bool, info os.FileMode) error {
+ for _, excludePattern := range excludePatterns {
+ excluded, err := doublestar.Match(excludePattern, filepath.ToSlash(name))
+ if err != nil {
+ return err
+ }
+ if excluded {
+ return godirwalk.SkipThis
+ }
+ }
+ if info.IsDir() && (info&os.ModeSymlink == 0) {
+ if err := f.watcher.Add(name); err != nil {
+ return errors.Wrapf(err, "failed adding watch to %v", name)
+ }
+ f.logger.Debug(fmt.Sprintf("watching directory %v", name))
+ }
+ if addMode == synthesizeEvents {
+ f.events <- Event{
+ Path: fs.AbsoluteSystemPathFromUpstream(name),
+ EventType: FileAdded,
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ f.allExcludes = append(f.allExcludes, excludePatterns...)
+
+ return nil
+}
+
+func (f *fsNotifyBackend) watch() {
+outer:
+ for {
+ select {
+ case ev, ok := <-f.watcher.Events:
+ if !ok {
+ break outer
+ }
+ eventType := toFileEvent(ev.Op)
+ path := fs.AbsoluteSystemPathFromUpstream(ev.Name)
+ if eventType == FileAdded {
+ if err := f.onFileAdded(path); err != nil {
+ f.errors <- err
+ }
+ }
+ f.events <- Event{
+ Path: path,
+ EventType: eventType,
+ }
+ case err, ok := <-f.watcher.Errors:
+ if !ok {
+ break outer
+ }
+ f.errors <- err
+ }
+ }
+}
+
+var _modifiedMask = fsnotify.Chmod | fsnotify.Write
+
+func toFileEvent(op fsnotify.Op) FileEvent {
+ if op&fsnotify.Create != 0 {
+ return FileAdded
+ } else if op&fsnotify.Remove != 0 {
+ return FileDeleted
+ } else if op&_modifiedMask != 0 {
+ return FileModified
+ } else if op&fsnotify.Rename != 0 {
+ return FileRenamed
+ }
+ return FileOther
+}
+
+func (f *fsNotifyBackend) Start() error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ if f.closed {
+ return ErrFilewatchingClosed
+ }
+ for _, dir := range f.watcher.WatchList() {
+ for _, excludePattern := range f.allExcludes {
+ excluded, err := doublestar.Match(excludePattern, filepath.ToSlash(dir))
+ if err != nil {
+ return err
+ }
+ if excluded {
+ if err := f.watcher.Remove(dir); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ go f.watch()
+ return nil
+}
+
+func (f *fsNotifyBackend) AddRoot(root turbopath.AbsoluteSystemPath, excludePatterns ...string) error {
+ // We don't synthesize events for the initial watch
+ return f.watchRecursively(root, excludePatterns, dontSynthesizeEvents)
+}
+
+// GetPlatformSpecificBackend returns a filewatching backend appropriate for the OS we are
+// running on.
+func GetPlatformSpecificBackend(logger hclog.Logger) (Backend, error) {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return nil, err
+ }
+ return &fsNotifyBackend{
+ watcher: watcher,
+ events: make(chan Event),
+ errors: make(chan error),
+ logger: logger.Named("fsnotify"),
+ }, nil
+}