diff options
Diffstat (limited to 'cli/internal/filewatcher/filewatcher.go')
| -rw-r--r-- | cli/internal/filewatcher/filewatcher.go | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/cli/internal/filewatcher/filewatcher.go b/cli/internal/filewatcher/filewatcher.go new file mode 100644 index 0000000..4f79495 --- /dev/null +++ b/cli/internal/filewatcher/filewatcher.go @@ -0,0 +1,167 @@ +// Package filewatcher is used to handle watching for file changes inside the monorepo +package filewatcher + +import ( + "path/filepath" + "strings" + "sync" + + "github.com/hashicorp/go-hclog" + "github.com/pkg/errors" + "github.com/vercel/turbo/cli/internal/turbopath" +) + +// _ignores is the set of paths we exempt from file-watching +var _ignores = []string{".git", "node_modules"} + +// FileWatchClient defines the callbacks used by the file watching loop. +// All methods are called from the same goroutine so they: +// 1) do not need synchronization +// 2) should minimize the work they are doing when called, if possible +type FileWatchClient interface { + OnFileWatchEvent(ev Event) + OnFileWatchError(err error) + OnFileWatchClosed() +} + +// FileEvent is an enum covering the kinds of things that can happen +// to files that we might be interested in +type FileEvent int + +const ( + // FileAdded - this is a new file + FileAdded FileEvent = iota + 1 + // FileDeleted - this file has been removed + FileDeleted + // FileModified - this file has been changed in some way + FileModified + // FileRenamed - a file's name has changed + FileRenamed + // FileOther - some other backend-specific event has happened + FileOther +) + +var ( + // ErrFilewatchingClosed is returned when filewatching has been closed + ErrFilewatchingClosed = errors.New("Close() has already been called for filewatching") + // ErrFailedToStart is returned when filewatching fails to start up + ErrFailedToStart = errors.New("filewatching failed to start") +) + +// Event is the backend-independent information about a file change +type Event struct { + Path turbopath.AbsoluteSystemPath + EventType FileEvent +} + +// Backend is the interface that describes what an underlying filesystem watching backend +// must provide. +type Backend interface { + AddRoot(root turbopath.AbsoluteSystemPath, excludePatterns ...string) error + Events() <-chan Event + Errors() <-chan error + Close() error + Start() error +} + +// FileWatcher handles watching all of the files in the monorepo. +// We currently ignore .git and top-level node_modules. We can revisit +// if necessary. +type FileWatcher struct { + backend Backend + + logger hclog.Logger + repoRoot turbopath.AbsoluteSystemPath + excludePattern string + + clientsMu sync.RWMutex + clients []FileWatchClient + closed bool +} + +// New returns a new FileWatcher instance +func New(logger hclog.Logger, repoRoot turbopath.AbsoluteSystemPath, backend Backend) *FileWatcher { + excludes := make([]string, len(_ignores)) + for i, ignore := range _ignores { + excludes[i] = filepath.ToSlash(repoRoot.UntypedJoin(ignore).ToString() + "/**") + } + excludePattern := "{" + strings.Join(excludes, ",") + "}" + return &FileWatcher{ + backend: backend, + logger: logger, + repoRoot: repoRoot, + excludePattern: excludePattern, + } +} + +// Close shuts down filewatching +func (fw *FileWatcher) Close() error { + return fw.backend.Close() +} + +// Start recursively adds all directories from the repo root, redacts the excluded ones, +// then fires off a goroutine to respond to filesystem events +func (fw *FileWatcher) Start() error { + if err := fw.backend.AddRoot(fw.repoRoot, fw.excludePattern); err != nil { + return err + } + if err := fw.backend.Start(); err != nil { + return err + } + go fw.watch() + return nil +} + +// AddRoot registers the root a filesystem hierarchy to be watched for changes. Events are *not* +// fired for existing files when AddRoot is called, only for subsequent changes. +// NOTE: if it appears helpful, we could change this behavior so that we provide a stream of initial +// events. +func (fw *FileWatcher) AddRoot(root turbopath.AbsoluteSystemPath, excludePatterns ...string) error { + return fw.backend.AddRoot(root, excludePatterns...) +} + +// watch is the main file-watching loop. Watching is not recursive, +// so when new directories are added, they are manually recursively watched. +func (fw *FileWatcher) watch() { +outer: + for { + select { + case ev, ok := <-fw.backend.Events(): + if !ok { + fw.logger.Info("Events channel closed. Exiting watch loop") + break outer + } + fw.clientsMu.RLock() + for _, client := range fw.clients { + client.OnFileWatchEvent(ev) + } + fw.clientsMu.RUnlock() + case err, ok := <-fw.backend.Errors(): + if !ok { + fw.logger.Info("Errors channel closed. Exiting watch loop") + break outer + } + fw.clientsMu.RLock() + for _, client := range fw.clients { + client.OnFileWatchError(err) + } + fw.clientsMu.RUnlock() + } + } + fw.clientsMu.Lock() + fw.closed = true + for _, client := range fw.clients { + client.OnFileWatchClosed() + } + fw.clientsMu.Unlock() +} + +// AddClient registers a client for filesystem events +func (fw *FileWatcher) AddClient(client FileWatchClient) { + fw.clientsMu.Lock() + defer fw.clientsMu.Unlock() + fw.clients = append(fw.clients, client) + if fw.closed { + client.OnFileWatchClosed() + } +} |
