aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/filewatcher/filewatcher.go
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/filewatcher/filewatcher.go
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'cli/internal/filewatcher/filewatcher.go')
-rw-r--r--cli/internal/filewatcher/filewatcher.go167
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()
+ }
+}