diff --git a/README.md b/README.md index 1f4eb583..f3fcc5b8 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,16 @@ -fsnotify is a Go library to provide cross-platform filesystem notifications on -Windows, Linux, macOS, BSD, and illumos. - -Go 1.17 or newer is required; the full documentation is at -https://pkg.go.dev/github.com/fsnotify/fsnotify +Fork of the original [fsnotify](https://github.com/fsnotify/fsnotify) library that enables recursive watching. +Because of the current implementation limitations, the only supported platforms are updated in the table below. --- Platform support: | Backend | OS | Status | -| :-------------------- | :--------- | :------------------------------------------------------------------------ | +| :-------------------- | :--------- |:--------------------------------------------------------------------------| | inotify | Linux | Supported | -| kqueue | BSD, macOS | Supported | +| kqueue | BSD, macOS | [Not Yet](https://github.com/fsnotify/fsnotify/issues/18) | | ReadDirectoryChangesW | Windows | Supported | -| FEN | illumos | Supported | +| FEN | illumos | [Not Yet](https://github.com/fsnotify/fsnotify/issues/18) | | fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) | | FSEvents | macOS | [Needs support in x/sys/unix][fsevents] | | USN Journals | Windows | [Needs support in x/sys/windows][usn] | @@ -35,7 +32,7 @@ package main import ( "log" - "github.com/fsnotify/fsnotify" + "github.com/e2b-dev/fsnotify" ) func main() { @@ -92,8 +89,7 @@ FAQ No, not unless you are watching the location it was moved to. ### Are subdirectories watched? -No, you must add watches for any directory you want to watch (a recursive -watcher is on the roadmap: [#18]). +Yes. The provided path has to end with "..." to watch subdirectories. [#18]: https://github.com/fsnotify/fsnotify/issues/18 diff --git a/backend_fen.go b/backend_fen.go index e43c6d08..c927c290 100644 --- a/backend_fen.go +++ b/backend_fen.go @@ -15,7 +15,7 @@ import ( "sync" "time" - "github.com/fsnotify/fsnotify/internal" + "github.com/e2b-dev/fsnotify/internal" "golang.org/x/sys/unix" ) diff --git a/backend_inotify.go b/backend_inotify.go index 4bba8773..a4975603 100644 --- a/backend_inotify.go +++ b/backend_inotify.go @@ -14,7 +14,7 @@ import ( "time" "unsafe" - "github.com/fsnotify/fsnotify/internal" + "github.com/e2b-dev/fsnotify/internal" "golang.org/x/sys/unix" ) @@ -248,6 +248,17 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error { w.sendEvent(Event{Name: root, Op: Create}) } + // If there's a dir for which we don't have read permissions, + // skip it silently. If it's the root directory itself, return an error. + _, infoErr := d.Info() + if infoErr != nil && errors.Is(infoErr, os.ErrPermission) { + if root == path { + return fmt.Errorf("fsnotify: not a readable directory: %q", path) + } + + return filepath.SkipDir // Skip this directory silently. + } + wf := flagRecurse if root == path { wf |= flagByUser @@ -510,6 +521,43 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs w.watches.wd[k] = ww } } + } else { + // Register a newly created dir tree + // This is for "mkdir -p one/two/three". + // Usually all those directories will be created before we can set up + // watchers on the subdirectories, so only "one" would be sent + // as a Create event and not "one/two" and "one/two/three" + // (inotifywait -r has the same problem). + err = filepath.WalkDir(ev.Name, func(curDir string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if ev.Name != curDir { + // Send artificial create event. + // We don't know what has really happened. + + // Send the previous event first to maintain proper ordering, then create a new event for the current directory + // The function expects to return only one event, so we need to send the previous event first and then create a new one + w.sendEvent(ev) + ev = w.newEvent(curDir, unix.IN_CREATE, 0) + } + + // If there's a dir for which we don't have read permissions, + // skip it silently. If it's the root directory itself, return an error. + _, infoErr := d.Info() + if infoErr != nil && errors.Is(infoErr, os.ErrPermission) { + return filepath.SkipDir // Skip this directory silently. + } + + if d.IsDir() { + return w.register(curDir, watch.flags, flagRecurse) + } + return nil + }) + if !w.sendError(err) { + return Event{}, false + } } } } diff --git a/backend_kqueue.go b/backend_kqueue.go index 4fc6dc5b..c26324b9 100644 --- a/backend_kqueue.go +++ b/backend_kqueue.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "github.com/fsnotify/fsnotify/internal" + "github.com/e2b-dev/fsnotify/internal" "golang.org/x/sys/unix" ) diff --git a/backend_windows.go b/backend_windows.go index 3433642d..8fc84207 100644 --- a/backend_windows.go +++ b/backend_windows.go @@ -18,7 +18,7 @@ import ( "time" "unsafe" - "github.com/fsnotify/fsnotify/internal" + "github.com/e2b-dev/fsnotify/internal" "golang.org/x/sys/windows" ) @@ -76,12 +76,16 @@ func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool } // Returns true if the error was sent, or false if watcher is closed. +// +// Close's reply channel is put back, as sendEvent does: the reader takes it +// on its next wakeup and runs the cleanup, and Close blocks until it does. func (w *readDirChangesW) sendError(err error) bool { if err == nil { return true } select { - case <-w.done: + case ch := <-w.done: + w.done <- ch return false case w.Errors <- err: return true @@ -89,11 +93,14 @@ func (w *readDirChangesW) sendError(err error) bool { } func (w *readDirChangesW) Close() error { - if w.isClosed() { + // Check and set under one lock: a second concurrent Close must not queue + // a second reply, which the one-slot done channel and the single reader + // cannot both serve. + w.mu.Lock() + if w.closed { + w.mu.Unlock() return nil } - - w.mu.Lock() w.closed = true w.mu.Unlock() diff --git a/backend_windows_test.go b/backend_windows_test.go index 6364f43a..362ab57a 100644 --- a/backend_windows_test.go +++ b/backend_windows_test.go @@ -3,9 +3,12 @@ package fsnotify import ( + "errors" "fmt" "strings" + "sync" "testing" + "time" ) func TestRemoveState(t *testing.T) { @@ -71,3 +74,91 @@ func TestWindowsRemWatch(t *testing.T) { t.Fatal("Should be fail with closed handle\n") } } + +// sendError on a closing watcher must leave Close's reply channel queued for +// the reader. If it is dropped, the reader never runs the shutdown cleanup and +// Close waits on the reply forever. +func TestSendErrorKeepsCloseReply(t *testing.T) { + w := &readDirChangesW{ + Errors: make(chan error), // nobody reads errors any more + done: make(chan chan<- error, 1), + } + reply := make(chan error) + w.done <- reply + + if w.sendError(errors.New("overflow")) { + t.Fatal("sendError reported the error as delivered") + } + select { + case got := <-w.done: + if got != reply { + t.Fatal("sendError replaced Close's reply channel") + } + default: + t.Fatal("sendError dropped Close's reply channel") + } +} + +// Close must return when the error consumer has gone away and the backend +// then reports an error: the reader keeps the reply and completes shutdown. +func TestCloseAfterUnreadError(t *testing.T) { + w, err := NewWatcher() + if err != nil { + t.Fatal(err) + } + b := w.b.(*readDirChangesW) + + // Close queues its reply and wakes the reader; an error raised before the + // reader takes it meets an Errors channel nobody reads. + b.mu.Lock() + b.closed = true + b.mu.Unlock() + reply := make(chan error) + b.done <- reply + if b.sendError(errors.New("overflow")) { + t.Fatal("sendError reported the error as delivered") + } + if err := b.wakeupReader(); err != nil { + t.Fatal(err) + } + + select { + case err := <-reply: + if err != nil { + t.Fatalf("shutdown: %v", err) + } + case <-time.After(10 * time.Second): + t.Fatal("the reader never completed shutdown after an undelivered error") + } +} + +// Concurrent Close calls must all return: only the first queues a reply. +func TestConcurrentClose(t *testing.T) { + w, err := NewWatcher() + if err != nil { + t.Fatal(err) + } + if err := w.Add(t.TempDir()); err != nil { + t.Fatal(err) + } + + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = w.Close() + }() + } + closed := make(chan struct{}) + go func() { wg.Wait(); close(closed) }() + + select { + case <-closed: + case <-time.After(10 * time.Second): + t.Fatal("concurrent Close calls did not all return") + } + if b := w.b.(*readDirChangesW); len(b.done) != 0 { + t.Fatalf("%d reply channels left on done", len(b.done)) + } +} diff --git a/cmd/fsnotify/dedup.go b/cmd/fsnotify/dedup.go index 67269d44..9ed2a4f3 100644 --- a/cmd/fsnotify/dedup.go +++ b/cmd/fsnotify/dedup.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "github.com/fsnotify/fsnotify" + "github.com/e2b-dev/fsnotify" ) // Depending on the system, a single "write" can generate many Write events; for diff --git a/cmd/fsnotify/file.go b/cmd/fsnotify/file.go index 357e6937..0ac39de7 100644 --- a/cmd/fsnotify/file.go +++ b/cmd/fsnotify/file.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - "github.com/fsnotify/fsnotify" + "github.com/e2b-dev/fsnotify" ) // Watch one or more files, but instead of watching the file directly it watches diff --git a/cmd/fsnotify/watch.go b/cmd/fsnotify/watch.go index 046a1331..5862add6 100644 --- a/cmd/fsnotify/watch.go +++ b/cmd/fsnotify/watch.go @@ -1,6 +1,6 @@ package main -import "github.com/fsnotify/fsnotify" +import "github.com/e2b-dev/fsnotify" // This is the most basic example: it prints events to the terminal as we // receive them. diff --git a/fsnotify.go b/fsnotify.go index c962a0fa..5975b290 100644 --- a/fsnotify.go +++ b/fsnotify.go @@ -468,12 +468,13 @@ func withOps(op Op) addOpt { return func(opt *withOpts) { opt.op = op } } -// "Internal" option for recursive watches on inotify. -func withCreate() addOpt { +// WithCreate "Internal" option for recursive watches on inotify. +func WithCreate() addOpt { return func(opt *withOpts) { opt.sendCreate = true } } -var enableRecurse = false +// Enable recursive folder watching +var enableRecurse = true // Check if this path is recursive (ends with "/..." or "\..."), and return the // path with the /... stripped. diff --git a/fsnotify_test.go b/fsnotify_test.go index 11d88b46..7bdff059 100644 --- a/fsnotify_test.go +++ b/fsnotify_test.go @@ -16,7 +16,7 @@ import ( "testing" "time" - "github.com/fsnotify/fsnotify/internal" + "github.com/e2b-dev/fsnotify/internal" ) func init() { diff --git a/go.mod b/go.mod index 44bf2798..869384c2 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/fsnotify/fsnotify +module github.com/e2b-dev/fsnotify go 1.19 diff --git a/helpers_test.go b/helpers_test.go index b22edca5..2f1d204a 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -13,8 +13,8 @@ import ( "testing" "time" - "github.com/fsnotify/fsnotify/internal" - "github.com/fsnotify/fsnotify/internal/ztest" + "github.com/e2b-dev/fsnotify/internal" + "github.com/e2b-dev/fsnotify/internal/ztest" ) // We wait a little bit after most commands; gives the system some time to sync