]> go.fuhry.dev Git - fsnotify.git/commitdiff
Update documentation and examples (#496)
authorMartin Tournoij <martin@arp242.net>
Wed, 10 Aug 2022 15:10:01 +0000 (17:10 +0200)
committerGitHub <noreply@github.com>
Wed, 10 Aug 2022 15:10:01 +0000 (17:10 +0200)
Update some documentation and add various examples. I added the examples
as subcommands of ./cmd/fsnotify, so they're directly runnable, and it's
at least guaranteed they compile.

This adds some simpler test cases so it's easier to verify it actually
works as documented on all platforms, and it adds internal.Debug() for
all platforms, which is useful in dev.

Fixes 49
Fixes 74
Fixes 94
Fixes 122
Fixes 238
Fixes 372
Fixes 401

26 files changed:
README.md
backend_fen.go
backend_inotify.go
backend_kqueue.go
backend_other.go
backend_windows.go
cmd/fsnotify/dedup.go [new file with mode: 0644]
cmd/fsnotify/file.go [new file with mode: 0644]
cmd/fsnotify/main.go
cmd/fsnotify/watch.go [new file with mode: 0644]
fsnotify.go
fsnotify_test.go
helpers_test.go
internal/darwin.go
internal/debug_darwin.go [new file with mode: 0644]
internal/debug_dragonfly.go [new file with mode: 0644]
internal/debug_freebsd.go [new file with mode: 0644]
internal/debug_kqueue.go [new file with mode: 0644]
internal/debug_linux.go [new file with mode: 0644]
internal/debug_netbsd.go [new file with mode: 0644]
internal/debug_openbsd.go [new file with mode: 0644]
internal/debug_windows.go [new file with mode: 0644]
internal/freebsd.go [new file with mode: 0644]
internal/unix.go
internal/windows.go
mkdoc.zsh [new file with mode: 0755]

index 833a0c7cc710f845ec8df41db29ff010773fc71b..ff8a3db730898d029bcffcbeaf4e304b6a517734 100644 (file)
--- a/README.md
+++ b/README.md
@@ -28,52 +28,55 @@ A basic example:
 package main
 
 import (
-       "log"
+    "log"
 
-       "github.com/fsnotify/fsnotify"
+    "github.com/fsnotify/fsnotify"
 )
 
 func main() {
-       watcher, err := fsnotify.NewWatcher()
-       if err != nil {
-               log.Fatal(err)
-       }
-       defer watcher.Close()
-
-       done := make(chan bool)
-       go func() {
-               for {
-                       select {
-                       case event, ok := <-watcher.Events:
-                               if !ok {
-                                       return
-                               }
-                               log.Println("event:", event)
-                               if event.Has(fsnotify.Write) {
-                                       log.Println("modified file:", event.Name)
-                               }
-                       case err, ok := <-watcher.Errors:
-                               if !ok {
-                                       return
-                               }
-                               log.Println("error:", err)
-                       }
-               }
-       }()
-
-       err = watcher.Add("/tmp")
-       if err != nil {
-               log.Fatal(err)
-       }
-       <-done
+    // Create new watcher.
+    watcher, err := fsnotify.NewWatcher()
+    if err != nil {
+        log.Fatal(err)
+    }
+    defer watcher.Close()
+
+    // Start listening for events.
+    go func() {
+        for {
+            select {
+            case event, ok := <-watcher.Events:
+                if !ok {
+                    return
+                }
+                log.Println("event:", event)
+                if event.Has(fsnotify.Write) {
+                    log.Println("modified file:", event.Name)
+                }
+            case err, ok := <-watcher.Errors:
+                if !ok {
+                    return
+                }
+                log.Println("error:", err)
+            }
+        }
+    }()
+
+    // Add a path.
+    err = watcher.Add("/tmp")
+    if err != nil {
+        log.Fatal(err)
+    }
+
+    // Block main goroutine forever.
+    <-make(chan struct{})
 }
 ```
 
-A slightly more expansive example can be found in [cmd/fsnotify](cmd/fsnotify),
-which can be run with:
+Some more examples can be found in [cmd/fsnotify](cmd/fsnotify), which can be
+run with:
 
-    # Watch the current directory (not recursive).
-    $ go run ./cmd/fsnotify .
+    % go run ./cmd/fsnotify
 
 FAQ
 ---
@@ -109,10 +112,10 @@ descriptors are closed. It will emit a CHMOD though:
     os.Remove("file")        // CHMOD
     fp.Close()               // REMOVE
 
-Linux: the `fs.inotify.max_user_watches` sysctl variable specifies the upper
-limit for the number of watches per user, and `fs.inotify.max_user_instances`
-specifies the maximum number of inotify instances per user. Every Watcher you
-create is an "instance", and every path you add is a "watch".
+The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for
+the number of watches per user, and `fs.inotify.max_user_instances` specifies
+the maximum number of inotify instances per user. Every Watcher you create is an
+"instance", and every path you add is a "watch".
 
 These are also exposed in /proc as `/proc/sys/fs/inotify/max_user_watches` and
 `/proc/sys/fs/inotify/max_user_instances`
index 58737a47a02b4a453bb6391fe1639c78c2f85775..af2d092890bd909408ae9166ec71f47512b35415 100644 (file)
@@ -8,12 +8,102 @@ import (
 )
 
 // Watcher watches a set of files, delivering events to a channel.
+//
+// A watcher should not be copied (e.g. pass it by pointer, rather than by
+// value).
+//
+// # Linux notes
+//
+// When a file is removed a Remove event won't be emitted until all file
+// descriptors are closed, and deletes will always emit a Chmod. For example:
+//
+//     fp := os.Open("file")
+//     os.Remove("file")        // Triggers Chmod
+//     fp.Close()               // Triggers Remove
+//
+// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
+// for the number of watches per user, and fs.inotify.max_user_instances
+// specifies the maximum number of inotify instances per user. Every Watcher you
+// create is an "instance", and every path you add is a "watch".
+//
+// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
+// /proc/sys/fs/inotify/max_user_instances
+//
+// To increase them you can use sysctl or write the value to the /proc file:
+//
+//     # Default values on Linux 5.18
+//     sysctl fs.inotify.max_user_watches=124983
+//     sysctl fs.inotify.max_user_instances=128
+//
+// To make the changes persist on reboot edit /etc/sysctl.conf or
+// /usr/lib/sysctl.d/50-default.conf (on some systemd systems):
+//
+//     fs.inotify.max_user_watches=124983
+//     fs.inotify.max_user_instances=128
+//
+// Reaching the limit will result in a "no space left on device" or "too many open
+// files" error.
+//
+// # kqueue notes (macOS, BSD)
+//
+// kqueue requires opening a file descriptor for every file that's being watched;
+// so if you're watching a directory with five files then that's six file
+// descriptors. You will run in to your system's "max open files" limit faster on
+// these platforms.
+//
+// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
+// control the maximum number of open files, as well as /etc/login.conf on BSD
+// systems.
+//
+// # macOS notes
+//
+// Spotlight indexing on macOS can result in multiple events (see [#15]). A
+// temporary workaround is to add your folder(s) to the "Spotlight Privacy
+// Settings" until we have a native FSEvents implementation (see [#11]).
+//
+// [#11]: https://github.com/fsnotify/fsnotify/issues/11
+// [#15]: https://github.com/fsnotify/fsnotify/issues/15
 type Watcher struct {
+       // Events sends the filesystem change events.
+       //
+       // fsnotify can send the following events; a "path" here can refer to a
+       // file, directory, symbolic link, or special files like a FIFO.
+       //
+       //   fsnotify.Create    A new path was created; this may be followed by one
+       //                      or more Write events if data also gets written to a
+       //                      file.
+       //
+       //   fsnotify.Remove    A path was removed.
+       //
+       //   fsnotify.Rename    A path was renamed. A rename is always sent with the
+       //                      old path as [Event.Name], and a Create event will be
+       //                      sent with the new name. Renames are only sent for
+       //                      paths that are currently watched; e.g. moving an
+       //                      unmonitored file into a monitored directory will
+       //                      show up as just a Create. Similarly, renaming a file
+       //                      to outside a monitored directory will show up as
+       //                      only a Rename.
+       //
+       //   fsnotify.Write     A file or named pipe was written to. A Truncate will
+       //                      also trigger a Write. A single "write action"
+       //                      initiated by the user may show up as one or multiple
+       //                      writes, depending on when the system syncs things to
+       //                      disk. For example when compiling a large Go program
+       //                      you may get hundreds of Write events, so you
+       //                      probably want to wait until you've stopped receiving
+       //                      them (see the dedup example in cmd/fsnotify).
+       //
+       //   fsnotify.Chmod     Attributes were changes (never sent on Windows). On
+       //                      Linux this is also sent when a file is removed (or
+       //                      more accurately, when a link to an inode is
+       //                      removed), and on kqueue when a file is truncated.
        Events chan Event
+
+       // Errors sends any errors.
        Errors chan error
 }
 
-// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
+// NewWatcher creates a new Watcher.
 func NewWatcher() (*Watcher, error) {
        return nil, errors.New("FEN based watcher not yet supported for fsnotify\n")
 }
@@ -23,12 +113,46 @@ func (w *Watcher) Close() error {
        return nil
 }
 
-// Add starts watching the named file or directory (non-recursively).
+// Add starts monitoring the path for changes.
+//
+// A path can only be watched once; attempting to watch it more than once will
+// return an error. Paths that do not yet exist on the filesystem cannot be
+// added. A watch will be automatically removed if the path is deleted.
+//
+// A path will remain watched if it gets renamed to somewhere else on the same
+// filesystem, but the monitor will get removed if the path gets deleted and
+// re-created.
+//
+// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
+// filesystems (/proc, /sys, etc.) generally don't work.
+//
+// # Watching directories
+//
+// All files in a directory are monitored, including new files that are created
+// after the watcher is started. Subdirectories are not watched (i.e. it's
+// non-recursive).
+//
+// # Watching files
+//
+// Watching individual files (rather than directories) is generally not
+// recommended as many tools update files atomically. Instead of "just" writing
+// to the file a temporary file will be written to first, and if successful the
+// temporary file is moved to to destination, removing the original, or some
+// variant thereof. The watcher on the original file is now lost, as it no
+// longer exists.
+//
+// Instead, watch the parent directory and use [Event.Name] to filter out files
+// you're not interested in. There is an example of this in cmd/fsnotify/file.go
 func (w *Watcher) Add(name string) error {
        return nil
 }
 
-// Remove stops watching the the named file or directory (non-recursively).
+// Remove stops monitoring the path for changes.
+//
+// Directories are always removed non-recursively. For example, if you added
+// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
+//
+// Removing a path that has not yet been added returns [ErrNonExistentWatch].
 func (w *Watcher) Remove(name string) error {
        return nil
 }
index 32a1870b7114a5c6170a5203734ad31d3d5f893a..74441914168bcadd7663aa1058996981ae6f28c1 100644 (file)
@@ -17,12 +17,103 @@ import (
 )
 
 // Watcher watches a set of files, delivering events to a channel.
+//
+// A watcher should not be copied (e.g. pass it by pointer, rather than by
+// value).
+//
+// # Linux notes
+//
+// When a file is removed a Remove event won't be emitted until all file
+// descriptors are closed, and deletes will always emit a Chmod. For example:
+//
+//     fp := os.Open("file")
+//     os.Remove("file")        // Triggers Chmod
+//     fp.Close()               // Triggers Remove
+//
+// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
+// for the number of watches per user, and fs.inotify.max_user_instances
+// specifies the maximum number of inotify instances per user. Every Watcher you
+// create is an "instance", and every path you add is a "watch".
+//
+// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
+// /proc/sys/fs/inotify/max_user_instances
+//
+// To increase them you can use sysctl or write the value to the /proc file:
+//
+//     # Default values on Linux 5.18
+//     sysctl fs.inotify.max_user_watches=124983
+//     sysctl fs.inotify.max_user_instances=128
+//
+// To make the changes persist on reboot edit /etc/sysctl.conf or
+// /usr/lib/sysctl.d/50-default.conf (on some systemd systems):
+//
+//     fs.inotify.max_user_watches=124983
+//     fs.inotify.max_user_instances=128
+//
+// Reaching the limit will result in a "no space left on device" or "too many open
+// files" error.
+//
+// # kqueue notes (macOS, BSD)
+//
+// kqueue requires opening a file descriptor for every file that's being watched;
+// so if you're watching a directory with five files then that's six file
+// descriptors. You will run in to your system's "max open files" limit faster on
+// these platforms.
+//
+// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
+// control the maximum number of open files, as well as /etc/login.conf on BSD
+// systems.
+//
+// # macOS notes
+//
+// Spotlight indexing on macOS can result in multiple events (see [#15]). A
+// temporary workaround is to add your folder(s) to the "Spotlight Privacy
+// Settings" until we have a native FSEvents implementation (see [#11]).
+//
+// [#11]: https://github.com/fsnotify/fsnotify/issues/11
+// [#15]: https://github.com/fsnotify/fsnotify/issues/15
 type Watcher struct {
+       // Events sends the filesystem change events.
+       //
+       // fsnotify can send the following events; a "path" here can refer to a
+       // file, directory, symbolic link, or special files like a FIFO.
+       //
+       //   fsnotify.Create    A new path was created; this may be followed by one
+       //                      or more Write events if data also gets written to a
+       //                      file.
+       //
+       //   fsnotify.Remove    A path was removed.
+       //
+       //   fsnotify.Rename    A path was renamed. A rename is always sent with the
+       //                      old path as [Event.Name], and a Create event will be
+       //                      sent with the new name. Renames are only sent for
+       //                      paths that are currently watched; e.g. moving an
+       //                      unmonitored file into a monitored directory will
+       //                      show up as just a Create. Similarly, renaming a file
+       //                      to outside a monitored directory will show up as
+       //                      only a Rename.
+       //
+       //   fsnotify.Write     A file or named pipe was written to. A Truncate will
+       //                      also trigger a Write. A single "write action"
+       //                      initiated by the user may show up as one or multiple
+       //                      writes, depending on when the system syncs things to
+       //                      disk. For example when compiling a large Go program
+       //                      you may get hundreds of Write events, so you
+       //                      probably want to wait until you've stopped receiving
+       //                      them (see the dedup example in cmd/fsnotify).
+       //
+       //   fsnotify.Chmod     Attributes were changes (never sent on Windows). On
+       //                      Linux this is also sent when a file is removed (or
+       //                      more accurately, when a link to an inode is
+       //                      removed), and on kqueue when a file is truncated.
+       Events chan Event
+
+       // Errors sends any errors.
+       Errors chan error
+
        // Store fd here as os.File.Read() will no longer return on close after
        // calling Fd(). See: https://github.com/golang/go/issues/26439
        fd          int
-       Events      chan Event
-       Errors      chan error
        mu          sync.Mutex // Map access
        inotifyFile *os.File
        watches     map[string]*watch // Map of inotify watches (key: path)
@@ -31,7 +122,7 @@ type Watcher struct {
        doneResp    chan struct{}     // Channel to respond to Close
 }
 
-// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
+// NewWatcher creates a new Watcher.
 func NewWatcher() (*Watcher, error) {
        // Create inotify fd
        // Need to set the FD to nonblocking mode in order for SetDeadline methods to work
@@ -110,7 +201,36 @@ func (w *Watcher) Close() error {
        return nil
 }
 
-// Add starts watching the named file or directory (non-recursively).
+// Add starts monitoring the path for changes.
+//
+// A path can only be watched once; attempting to watch it more than once will
+// return an error. Paths that do not yet exist on the filesystem cannot be
+// added. A watch will be automatically removed if the path is deleted.
+//
+// A path will remain watched if it gets renamed to somewhere else on the same
+// filesystem, but the monitor will get removed if the path gets deleted and
+// re-created.
+//
+// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
+// filesystems (/proc, /sys, etc.) generally don't work.
+//
+// # Watching directories
+//
+// All files in a directory are monitored, including new files that are created
+// after the watcher is started. Subdirectories are not watched (i.e. it's
+// non-recursive).
+//
+// # Watching files
+//
+// Watching individual files (rather than directories) is generally not
+// recommended as many tools update files atomically. Instead of "just" writing
+// to the file a temporary file will be written to first, and if successful the
+// temporary file is moved to to destination, removing the original, or some
+// variant thereof. The watcher on the original file is now lost, as it no
+// longer exists.
+//
+// Instead, watch the parent directory and use [Event.Name] to filter out files
+// you're not interested in. There is an example of this in cmd/fsnotify/file.go
 func (w *Watcher) Add(name string) error {
        name = filepath.Clean(name)
        if w.isClosed() {
@@ -143,7 +263,12 @@ func (w *Watcher) Add(name string) error {
        return nil
 }
 
-// Remove stops watching the named file or directory (non-recursively).
+// Remove stops monitoring the path for changes.
+//
+// Directories are always removed non-recursively. For example, if you added
+// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
+//
+// Removing a path that has not yet been added returns [ErrNonExistentWatch].
 func (w *Watcher) Remove(name string) error {
        name = filepath.Clean(name)
 
index e59b5c48130191afe5f500208b867f7cedee643e..feb570506ad0818d6e4bb7cd3b1e45539d54ee0e 100644 (file)
@@ -15,14 +15,103 @@ import (
 )
 
 // Watcher watches a set of files, delivering events to a channel.
+//
+// A watcher should not be copied (e.g. pass it by pointer, rather than by
+// value).
+//
+// # Linux notes
+//
+// When a file is removed a Remove event won't be emitted until all file
+// descriptors are closed, and deletes will always emit a Chmod. For example:
+//
+//     fp := os.Open("file")
+//     os.Remove("file")        // Triggers Chmod
+//     fp.Close()               // Triggers Remove
+//
+// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
+// for the number of watches per user, and fs.inotify.max_user_instances
+// specifies the maximum number of inotify instances per user. Every Watcher you
+// create is an "instance", and every path you add is a "watch".
+//
+// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
+// /proc/sys/fs/inotify/max_user_instances
+//
+// To increase them you can use sysctl or write the value to the /proc file:
+//
+//     # Default values on Linux 5.18
+//     sysctl fs.inotify.max_user_watches=124983
+//     sysctl fs.inotify.max_user_instances=128
+//
+// To make the changes persist on reboot edit /etc/sysctl.conf or
+// /usr/lib/sysctl.d/50-default.conf (on some systemd systems):
+//
+//     fs.inotify.max_user_watches=124983
+//     fs.inotify.max_user_instances=128
+//
+// Reaching the limit will result in a "no space left on device" or "too many open
+// files" error.
+//
+// # kqueue notes (macOS, BSD)
+//
+// kqueue requires opening a file descriptor for every file that's being watched;
+// so if you're watching a directory with five files then that's six file
+// descriptors. You will run in to your system's "max open files" limit faster on
+// these platforms.
+//
+// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
+// control the maximum number of open files, as well as /etc/login.conf on BSD
+// systems.
+//
+// # macOS notes
+//
+// Spotlight indexing on macOS can result in multiple events (see [#15]). A
+// temporary workaround is to add your folder(s) to the "Spotlight Privacy
+// Settings" until we have a native FSEvents implementation (see [#11]).
+//
+// [#11]: https://github.com/fsnotify/fsnotify/issues/11
+// [#15]: https://github.com/fsnotify/fsnotify/issues/15
 type Watcher struct {
+       // Events sends the filesystem change events.
+       //
+       // fsnotify can send the following events; a "path" here can refer to a
+       // file, directory, symbolic link, or special files like a FIFO.
+       //
+       //   fsnotify.Create    A new path was created; this may be followed by one
+       //                      or more Write events if data also gets written to a
+       //                      file.
+       //
+       //   fsnotify.Remove    A path was removed.
+       //
+       //   fsnotify.Rename    A path was renamed. A rename is always sent with the
+       //                      old path as [Event.Name], and a Create event will be
+       //                      sent with the new name. Renames are only sent for
+       //                      paths that are currently watched; e.g. moving an
+       //                      unmonitored file into a monitored directory will
+       //                      show up as just a Create. Similarly, renaming a file
+       //                      to outside a monitored directory will show up as
+       //                      only a Rename.
+       //
+       //   fsnotify.Write     A file or named pipe was written to. A Truncate will
+       //                      also trigger a Write. A single "write action"
+       //                      initiated by the user may show up as one or multiple
+       //                      writes, depending on when the system syncs things to
+       //                      disk. For example when compiling a large Go program
+       //                      you may get hundreds of Write events, so you
+       //                      probably want to wait until you've stopped receiving
+       //                      them (see the dedup example in cmd/fsnotify).
+       //
+       //   fsnotify.Chmod     Attributes were changes (never sent on Windows). On
+       //                      Linux this is also sent when a file is removed (or
+       //                      more accurately, when a link to an inode is
+       //                      removed), and on kqueue when a file is truncated.
        Events chan Event
-       Errors chan error
-       done   chan struct{}
 
-       kq        int    // File descriptor (as returned by the kqueue() syscall).
-       closepipe [2]int // Pipe used for closing.
+       // Errors sends any errors.
+       Errors chan error
 
+       done         chan struct{}
+       kq           int                         // File descriptor (as returned by the kqueue() syscall).
+       closepipe    [2]int                      // Pipe used for closing.
        mu           sync.Mutex                  // Protects access to watcher data
        watches      map[string]int              // Watched file descriptors (key: path).
        watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
@@ -38,7 +127,7 @@ type pathInfo struct {
        isDir bool
 }
 
-// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
+// NewWatcher creates a new Watcher.
 func NewWatcher() (*Watcher, error) {
        kq, closepipe, err := newKqueue()
        if err != nil {
@@ -144,7 +233,36 @@ func (w *Watcher) Close() error {
        return nil
 }
 
-// Add starts watching the named file or directory (non-recursively).
+// Add starts monitoring the path for changes.
+//
+// A path can only be watched once; attempting to watch it more than once will
+// return an error. Paths that do not yet exist on the filesystem cannot be
+// added. A watch will be automatically removed if the path is deleted.
+//
+// A path will remain watched if it gets renamed to somewhere else on the same
+// filesystem, but the monitor will get removed if the path gets deleted and
+// re-created.
+//
+// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
+// filesystems (/proc, /sys, etc.) generally don't work.
+//
+// # Watching directories
+//
+// All files in a directory are monitored, including new files that are created
+// after the watcher is started. Subdirectories are not watched (i.e. it's
+// non-recursive).
+//
+// # Watching files
+//
+// Watching individual files (rather than directories) is generally not
+// recommended as many tools update files atomically. Instead of "just" writing
+// to the file a temporary file will be written to first, and if successful the
+// temporary file is moved to to destination, removing the original, or some
+// variant thereof. The watcher on the original file is now lost, as it no
+// longer exists.
+//
+// Instead, watch the parent directory and use [Event.Name] to filter out files
+// you're not interested in. There is an example of this in cmd/fsnotify/file.go
 func (w *Watcher) Add(name string) error {
        w.mu.Lock()
        w.userWatches[name] = struct{}{}
@@ -153,7 +271,12 @@ func (w *Watcher) Add(name string) error {
        return err
 }
 
-// Remove stops watching the the named file or directory (non-recursively).
+// Remove stops monitoring the path for changes.
+//
+// Directories are always removed non-recursively. For example, if you added
+// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
+//
+// Removing a path that has not yet been added returns [ErrNonExistentWatch].
 func (w *Watcher) Remove(name string) error {
        name = filepath.Clean(name)
        w.mu.Lock()
index d7b4c17c01e7b65fdd1c181196299044bf3f437f..d27f2809e6eb2bd914e1acae6cd57bc97b7714a0 100644 (file)
@@ -11,7 +11,7 @@ import (
 // Watcher watches a set of files, delivering events to a channel.
 type Watcher struct{}
 
-// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
+// NewWatcher creates a new Watcher.
 func NewWatcher() (*Watcher, error) {
        return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS)
 }
@@ -21,12 +21,46 @@ func (w *Watcher) Close() error {
        return nil
 }
 
-// Add starts watching the named file or directory (non-recursively).
+// Add starts monitoring the path for changes.
+//
+// A path can only be watched once; attempting to watch it more than once will
+// return an error. Paths that do not yet exist on the filesystem cannot be
+// added. A watch will be automatically removed if the path is deleted.
+//
+// A path will remain watched if it gets renamed to somewhere else on the same
+// filesystem, but the monitor will get removed if the path gets deleted and
+// re-created.
+//
+// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
+// filesystems (/proc, /sys, etc.) generally don't work.
+//
+// # Watching directories
+//
+// All files in a directory are monitored, including new files that are created
+// after the watcher is started. Subdirectories are not watched (i.e. it's
+// non-recursive).
+//
+// # Watching files
+//
+// Watching individual files (rather than directories) is generally not
+// recommended as many tools update files atomically. Instead of "just" writing
+// to the file a temporary file will be written to first, and if successful the
+// temporary file is moved to to destination, removing the original, or some
+// variant thereof. The watcher on the original file is now lost, as it no
+// longer exists.
+//
+// Instead, watch the parent directory and use [Event.Name] to filter out files
+// you're not interested in. There is an example of this in cmd/fsnotify/file.go
 func (w *Watcher) Add(name string) error {
        return nil
 }
 
-// Remove stops watching the the named file or directory (non-recursively).
+// Remove stops monitoring the path for changes.
+//
+// Directories are always removed non-recursively. For example, if you added
+// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
+//
+// Removing a path that has not yet been added returns [ErrNonExistentWatch].
 func (w *Watcher) Remove(name string) error {
        return nil
 }
index d503a4052a9f1012a49e8c493dee79b6dae5cf75..c52272b2ae8af227b5716e134c010585932e5c91 100644 (file)
@@ -18,8 +18,98 @@ import (
 )
 
 // Watcher watches a set of files, delivering events to a channel.
+//
+// A watcher should not be copied (e.g. pass it by pointer, rather than by
+// value).
+//
+// # Linux notes
+//
+// When a file is removed a Remove event won't be emitted until all file
+// descriptors are closed, and deletes will always emit a Chmod. For example:
+//
+//     fp := os.Open("file")
+//     os.Remove("file")        // Triggers Chmod
+//     fp.Close()               // Triggers Remove
+//
+// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
+// for the number of watches per user, and fs.inotify.max_user_instances
+// specifies the maximum number of inotify instances per user. Every Watcher you
+// create is an "instance", and every path you add is a "watch".
+//
+// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
+// /proc/sys/fs/inotify/max_user_instances
+//
+// To increase them you can use sysctl or write the value to the /proc file:
+//
+//     # Default values on Linux 5.18
+//     sysctl fs.inotify.max_user_watches=124983
+//     sysctl fs.inotify.max_user_instances=128
+//
+// To make the changes persist on reboot edit /etc/sysctl.conf or
+// /usr/lib/sysctl.d/50-default.conf (on some systemd systems):
+//
+//     fs.inotify.max_user_watches=124983
+//     fs.inotify.max_user_instances=128
+//
+// Reaching the limit will result in a "no space left on device" or "too many open
+// files" error.
+//
+// # kqueue notes (macOS, BSD)
+//
+// kqueue requires opening a file descriptor for every file that's being watched;
+// so if you're watching a directory with five files then that's six file
+// descriptors. You will run in to your system's "max open files" limit faster on
+// these platforms.
+//
+// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
+// control the maximum number of open files, as well as /etc/login.conf on BSD
+// systems.
+//
+// # macOS notes
+//
+// Spotlight indexing on macOS can result in multiple events (see [#15]). A
+// temporary workaround is to add your folder(s) to the "Spotlight Privacy
+// Settings" until we have a native FSEvents implementation (see [#11]).
+//
+// [#11]: https://github.com/fsnotify/fsnotify/issues/11
+// [#15]: https://github.com/fsnotify/fsnotify/issues/15
 type Watcher struct {
+       // Events sends the filesystem change events.
+       //
+       // fsnotify can send the following events; a "path" here can refer to a
+       // file, directory, symbolic link, or special files like a FIFO.
+       //
+       //   fsnotify.Create    A new path was created; this may be followed by one
+       //                      or more Write events if data also gets written to a
+       //                      file.
+       //
+       //   fsnotify.Remove    A path was removed.
+       //
+       //   fsnotify.Rename    A path was renamed. A rename is always sent with the
+       //                      old path as [Event.Name], and a Create event will be
+       //                      sent with the new name. Renames are only sent for
+       //                      paths that are currently watched; e.g. moving an
+       //                      unmonitored file into a monitored directory will
+       //                      show up as just a Create. Similarly, renaming a file
+       //                      to outside a monitored directory will show up as
+       //                      only a Rename.
+       //
+       //   fsnotify.Write     A file or named pipe was written to. A Truncate will
+       //                      also trigger a Write. A single "write action"
+       //                      initiated by the user may show up as one or multiple
+       //                      writes, depending on when the system syncs things to
+       //                      disk. For example when compiling a large Go program
+       //                      you may get hundreds of Write events, so you
+       //                      probably want to wait until you've stopped receiving
+       //                      them (see the dedup example in cmd/fsnotify).
+       //
+       //   fsnotify.Chmod     Attributes were changes (never sent on Windows). On
+       //                      Linux this is also sent when a file is removed (or
+       //                      more accurately, when a link to an inode is
+       //                      removed), and on kqueue when a file is truncated.
        Events chan Event
+
+       // Errors sends any errors.
        Errors chan error
 
        port  windows.Handle // Handle to completion port
@@ -31,7 +121,7 @@ type Watcher struct {
        isClosed bool       // Set to true when Close() is first called
 }
 
-// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
+// NewWatcher creates a new Watcher.
 func NewWatcher() (*Watcher, error) {
        port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
        if err != nil {
@@ -92,7 +182,36 @@ func (w *Watcher) Close() error {
        return <-ch
 }
 
-// Add starts watching the named file or directory (non-recursively).
+// Add starts monitoring the path for changes.
+//
+// A path can only be watched once; attempting to watch it more than once will
+// return an error. Paths that do not yet exist on the filesystem cannot be
+// added. A watch will be automatically removed if the path is deleted.
+//
+// A path will remain watched if it gets renamed to somewhere else on the same
+// filesystem, but the monitor will get removed if the path gets deleted and
+// re-created.
+//
+// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
+// filesystems (/proc, /sys, etc.) generally don't work.
+//
+// # Watching directories
+//
+// All files in a directory are monitored, including new files that are created
+// after the watcher is started. Subdirectories are not watched (i.e. it's
+// non-recursive).
+//
+// # Watching files
+//
+// Watching individual files (rather than directories) is generally not
+// recommended as many tools update files atomically. Instead of "just" writing
+// to the file a temporary file will be written to first, and if successful the
+// temporary file is moved to to destination, removing the original, or some
+// variant thereof. The watcher on the original file is now lost, as it no
+// longer exists.
+//
+// Instead, watch the parent directory and use [Event.Name] to filter out files
+// you're not interested in. There is an example of this in cmd/fsnotify/file.go
 func (w *Watcher) Add(name string) error {
        w.mu.Lock()
        if w.isClosed {
@@ -114,7 +233,12 @@ func (w *Watcher) Add(name string) error {
        return <-in.reply
 }
 
-// Remove stops watching the the named file or directory (non-recursively).
+// Remove stops monitoring the path for changes.
+//
+// Directories are always removed non-recursively. For example, if you added
+// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
+//
+// Removing a path that has not yet been added returns [ErrNonExistentWatch].
 func (w *Watcher) Remove(name string) error {
        in := &input{
                op:    opRemoveWatch,
diff --git a/cmd/fsnotify/dedup.go b/cmd/fsnotify/dedup.go
new file mode 100644 (file)
index 0000000..2d790a9
--- /dev/null
@@ -0,0 +1,102 @@
+package main
+
+import (
+       "math"
+       "sync"
+       "time"
+
+       "github.com/fsnotify/fsnotify"
+)
+
+// Depending on the system, a single "write" can generate many Write events; for
+// example compiling a large Go program can generate hundreds of Write events.
+//
+// The general strategy to deal with this is to wait a short time for more write
+// events, resetting the wait period for every new event.
+func dedup(paths ...string) {
+       if len(paths) < 1 {
+               exit("must specify at least one path to watch")
+       }
+
+       // Create a new watcher.
+       w, err := fsnotify.NewWatcher()
+       if err != nil {
+               exit("creating a new watcher: %s", err)
+       }
+       defer w.Close()
+
+       // Start listening for events.
+       go dedupLoop(w)
+
+       // Add all paths.
+       for _, p := range paths {
+               err = w.Add(p)
+               if err != nil {
+                       exit("%q: %s", p, err)
+               }
+       }
+
+       printTime("ready; press ^C to exit")
+       <-make(chan struct{}) // Block forever
+}
+
+func dedupLoop(w *fsnotify.Watcher) {
+       var (
+               // Wait 100ms for new events; each new event resets the timer.
+               waitFor = 100 * time.Millisecond
+
+               // Keep track of the timers, as path → timer.
+               mu     sync.Mutex
+               timers = make(map[string]*time.Timer)
+
+               // Callback we run.
+               printEvent = func(e fsnotify.Event) {
+                       printTime(e.String())
+
+                       // Don't need to remove the timer if you don't have a lot of files.
+                       mu.Lock()
+                       delete(timers, e.Name)
+                       mu.Unlock()
+               }
+       )
+
+       for {
+               select {
+               // Read from Errors.
+               case err, ok := <-w.Errors:
+                       if !ok { // Channel was closed (i.e. Watcher.Close() was called).
+                               return
+                       }
+                       printTime("ERROR: %s", err)
+               // Read from Events.
+               case e, ok := <-w.Events:
+                       if !ok { // Channel was closed (i.e. Watcher.Close() was called).
+                               return
+                       }
+
+                       // We just want to watch for file creation, so ignore everything
+                       // outside of Create and Write.
+                       if !e.Has(fsnotify.Create) && !e.Has(fsnotify.Write) {
+                               continue
+                       }
+
+                       // Get timer.
+                       mu.Lock()
+                       t, ok := timers[e.Name]
+                       mu.Unlock()
+
+                       // No timer yet, so create one.
+                       if !ok {
+                               t = time.AfterFunc(math.MaxInt64, func() { printEvent(e) })
+                               t.Stop()
+
+                               mu.Lock()
+                               timers[e.Name] = t
+                               mu.Unlock()
+                       }
+
+                       // Reset the timer for this path, so it will start from 100ms again.
+                       t.Reset(waitFor)
+               }
+       }
+}
diff --git a/cmd/fsnotify/file.go b/cmd/fsnotify/file.go
new file mode 100644 (file)
index 0000000..0f0b448
--- /dev/null
@@ -0,0 +1,82 @@
+package main
+
+import (
+       "os"
+       "path/filepath"
+
+       "github.com/fsnotify/fsnotify"
+)
+
+func file(files ...string) {
+       if len(files) < 1 {
+               exit("must specify at least one file to watch")
+       }
+
+       // Create a new watcher.
+       w, err := fsnotify.NewWatcher()
+       if err != nil {
+               exit("creating a new watcher: %s", err)
+       }
+       defer w.Close()
+
+       // Start listening for events.
+       go fileLoop(w, files)
+
+       // Add all files.
+       for _, p := range files {
+               st, err := os.Lstat(p)
+               if err != nil {
+                       exit("%s", err)
+               }
+
+               if st.IsDir() {
+                       exit("%q is a directory, not a file", p)
+               }
+
+               // Watch the directory, not the file itself.
+               err = w.Add(filepath.Dir(p))
+               if err != nil {
+                       exit("%q: %s", p, err)
+               }
+       }
+
+       printTime("ready; press ^C to exit")
+       <-make(chan struct{}) // Block forever
+}
+
+func fileLoop(w *fsnotify.Watcher, files []string) {
+       i := 0
+       for {
+               select {
+               // Read from Errors.
+               case err, ok := <-w.Errors:
+                       if !ok { // Channel was closed (i.e. Watcher.Close() was called).
+                               return
+                       }
+                       printTime("ERROR: %s", err)
+               // Read from Events.
+               case e, ok := <-w.Events:
+                       if !ok { // Channel was closed (i.e. Watcher.Close() was called).
+                               return
+                       }
+
+                       // Ignore files we're not interested in. Can use a
+                       // map[string]struct{} if you have a lot of files, but for just a
+                       // few files simply looping over a slice is faster.
+                       var found bool
+                       for _, f := range files {
+                               if f == e.Name {
+                                       found = true
+                               }
+                       }
+                       if !found {
+                               continue
+                       }
+
+                       // Just print the event nicely aligned, and keep track how many
+                       // events we've seen.
+                       i++
+                       printTime("%3d %s", i, e)
+               }
+       }
+}
index 688779316783baeb162f9567dbd16f8e1c759ff0..5c99ced0b3da19b18f163a70046cab16ab50ea15 100644 (file)
@@ -1,67 +1,64 @@
 package main
 
 import (
-       "errors"
        "fmt"
        "os"
        "path/filepath"
        "time"
-
-       "github.com/fsnotify/fsnotify"
 )
 
-func fatal(err error) {
-       if err == nil {
-               return
-       }
-       fmt.Fprintf(os.Stderr, "%s: %s\n", filepath.Base(os.Args[0]), err)
+var usage = `
+fsnotify is a library to provide cross-platform file system notifications for
+Go. This utility serves as an example and debugging tool.
+
+https://github.com/fsnotify/fsnotify
+
+Commands:
+
+    watch [paths]  Watch the paths for changes and print the events.
+    file  [file]   Watch a single file for changes.
+    dedup [paths]  Watch the paths for changes, suppressing duplicate events.
+`[1:]
+
+func exit(format string, a ...interface{}) {
+       fmt.Fprintf(os.Stderr, filepath.Base(os.Args[0])+": "+format+"\n", a...)
+       fmt.Print("\n" + usage)
        os.Exit(1)
 }
 
-func line(s string, args ...interface{}) {
+func help() {
+       fmt.Printf("%s [command] [arguments]\n\n", filepath.Base(os.Args[0]))
+       fmt.Print(usage)
+       os.Exit(0)
+}
+
+// Print line prefixed with the time (a bit shorter than log.Print; we don't
+// really need the date and ms is useful here).
+func printTime(s string, args ...interface{}) {
        fmt.Printf(time.Now().Format("15:04:05.0000")+" "+s+"\n", args...)
 }
 
 func main() {
-       if len(os.Args) < 2 {
-               fatal(errors.New("must specify at least one path to watch"))
+       if len(os.Args) == 1 {
+               help()
        }
-
-       w, err := fsnotify.NewWatcher()
-       fatal(err)
-       defer w.Close()
-
-       go func() {
-               i := 0
-               for {
-                       select {
-                       case e, ok := <-w.Events:
-                               if !ok {
-                                       return
-                               }
-
-                               i++
-                               m := ""
-                               if e.Has(fsnotify.Write) {
-                                       m = "(modified)"
-                               }
-                               line("%3d %-10s %-10s %q", i, e.Op, m, e.Name)
-                       case err, ok := <-w.Errors:
-                               if !ok {
-                                       return
-                               }
-                               line("ERROR: %s", err)
-                       }
-               }
-       }()
-
-       for _, p := range os.Args[1:] {
-               err = w.Add(p)
-               if err != nil {
-                       fatal(fmt.Errorf("%q: %w", p, err))
+       // Always show help if -h[elp] appears anywhere before we do anything else.
+       for _, f := range os.Args[1:] {
+               switch f {
+               case "help", "-h", "-help", "--help":
+                       help()
                }
        }
 
-       line("watching; press ^C to exit")
-       <-make(chan struct{})
+       cmd, args := os.Args[1], os.Args[2:]
+       switch cmd {
+       default:
+               exit("unknown command: %q", cmd)
+       case "watch":
+               watch(args...)
+       case "file":
+               file(args...)
+       case "dedup":
+               dedup(args...)
+       }
 }
diff --git a/cmd/fsnotify/watch.go b/cmd/fsnotify/watch.go
new file mode 100644 (file)
index 0000000..3fe50e1
--- /dev/null
@@ -0,0 +1,56 @@
+package main
+
+import "github.com/fsnotify/fsnotify"
+
+// This is the most basic example: it prints events to the terminal as we
+// receive them.
+func watch(paths ...string) {
+       if len(paths) < 1 {
+               exit("must specify at least one path to watch")
+       }
+
+       // Create a new watcher.
+       w, err := fsnotify.NewWatcher()
+       if err != nil {
+               exit("creating a new watcher: %s", err)
+       }
+       defer w.Close()
+
+       // Start listening for events.
+       go watchLoop(w)
+
+       // Add all paths.
+       for _, p := range paths {
+               err = w.Add(p)
+               if err != nil {
+                       exit("%q: %s", p, err)
+               }
+       }
+
+       printTime("ready; press ^C to exit")
+       <-make(chan struct{}) // Block forever
+}
+
+func watchLoop(w *fsnotify.Watcher) {
+       i := 0
+       for {
+               select {
+               // Read from Errors.
+               case err, ok := <-w.Errors:
+                       if !ok { // Channel was closed (i.e. Watcher.Close() was called).
+                               return
+                       }
+                       printTime("ERROR: %s", err)
+               // Read from Events.
+               case e, ok := <-w.Events:
+                       if !ok { // Channel was closed (i.e. Watcher.Close() was called).
+                               return
+                       }
+
+                       // Just print the event nicely aligned, and keep track how many
+                       // events we've seen.
+                       i++
+                       printTime("%3d %s", i, e)
+               }
+       }
+}
index 6c49b4307b0a87e4420e228b11cef118e0170d56..f776471578e1bcfc1e42f51977541ba76c3a8b50 100644 (file)
@@ -11,7 +11,7 @@ import (
        "strings"
 )
 
-// Event represents a single file system notification.
+// Event represents a file system notification.
 type Event struct {
        // Path to the file or directory.
        //
@@ -23,14 +23,15 @@ type Event struct {
        // File operation that triggered the event.
        //
        // This is a bitmask as some systems may send multiple operations at once.
-       // Use the Op.Has() or Event.Has() method instead of comparing with ==.
+       // Use the Event.Has() method instead of comparing with ==.
        Op Op
 }
 
 // Op describes a set of file operations.
 type Op uint32
 
-// These are the generalized file operations that can trigger a notification.
+// The operations fsnotify can trigger; see the documentation on [Watcher] for a
+// full description, and check them with [Event.Has].
 const (
        Create Op = 1 << iota
        Write
@@ -63,7 +64,7 @@ func (op Op) String() string {
                b.WriteString("|CHMOD")
        }
        if b.Len() == 0 {
-               return ""
+               return "[no events]"
        }
        return b.String()[1:]
 }
@@ -74,8 +75,7 @@ func (o Op) Has(h Op) bool { return o&h == h }
 // Has reports if this event has the given operation.
 func (e Event) Has(op Op) bool { return e.Op.Has(op) }
 
-// String returns a string representation of the event in the form
-// "file: REMOVE|WRITE|..."
+// String returns a string representation of the event with their path.
 func (e Event) String() string {
-       return fmt.Sprintf("%q: %s", e.Name, e.Op.String())
+       return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
 }
index 51ce2a1e8e4b96944e3c4b47a0374a1d56024ccc..993864e4bc9c62d7a69305a043ab5b71510636b9 100644 (file)
@@ -87,9 +87,9 @@ func TestWatch(t *testing.T) {
 
                        # TODO: not sure why the REMOVE /sub is dropped.
                        dragonfly:
-                create    /sub
-                create    /file
-                remove    /file
+                               create    /sub
+                               create    /file
+                               remove    /file
 
                        # Windows includes a write for the /sub dir too, two of them even(?)
                        windows:
@@ -122,8 +122,8 @@ func TestWatch(t *testing.T) {
                        # We never set up a watcher on the unreadable file, so we don't get
                        # the REMOVE.
                        kqueue:
-                WRITE    "/file"
-                REMOVE   "/file"
+                               WRITE    "/file"
+                               REMOVE   "/file"
                `},
 
                {"watch same dir twice", func(t *testing.T, w *Watcher, tmp string) {
@@ -160,22 +160,168 @@ func TestWatch(t *testing.T) {
        }
 }
 
-func TestWatchRename(t *testing.T) {
+func TestWatchCreate(t *testing.T) {
        tests := []testCase{
-               {"rename file", func(t *testing.T, w *Watcher, tmp string) {
+               // Files
+               {"create empty file", func(t *testing.T, w *Watcher, tmp string) {
+                       addWatch(t, w, tmp)
+                       touch(t, tmp, "file")
+               }, `
+                       create  /file
+               `},
+               {"create file with data", func(t *testing.T, w *Watcher, tmp string) {
+                       addWatch(t, w, tmp)
+                       cat(t, "data", tmp, "file")
+               }, `
+                       create  /file
+                       write   /file
+               `},
+
+               // Directories
+               {"create new directory", func(t *testing.T, w *Watcher, tmp string) {
+                       addWatch(t, w, tmp)
+                       mkdir(t, tmp, "dir")
+               }, `
+                       create  /dir
+               `},
+
+               // Links
+               {"create new symlink to file", func(t *testing.T, w *Watcher, tmp string) {
+                       touch(t, tmp, "file")
+                       addWatch(t, w, tmp)
+                       symlink(t, filepath.Join(tmp, "file"), tmp, "link")
+               }, `
+                       create  /link
+
+                       windows:
+                               create   /link
+                               write    /link
+               `},
+               {"create new symlink to directory", func(t *testing.T, w *Watcher, tmp string) {
+                       addWatch(t, w, tmp)
+                       symlink(t, tmp, tmp, "link")
+               }, `
+                       create  /link
+
+                       windows:
+                               create  /link
+                               write  /link
+               `},
+
+               // FIFO
+               {"create new named pipe", func(t *testing.T, w *Watcher, tmp string) {
+                       if runtime.GOOS == "windows" {
+                               t.Skip("no named pipes on windows")
+                       }
+                       touch(t, tmp, "file")
+                       addWatch(t, w, tmp)
+                       mkfifo(t, tmp, "fifo")
+               }, `
+                       create  /fifo
+               `},
+               // Device node
+               {"create new device node pipe", func(t *testing.T, w *Watcher, tmp string) {
+                       if runtime.GOOS == "windows" {
+                               t.Skip("no device nodes on windows")
+                       }
+                       if isKqueue() {
+                               t.Skip("needs root on BSD")
+                       }
+                       touch(t, tmp, "file")
+                       addWatch(t, w, tmp)
+
+                       mknod(t, 0, tmp, "dev")
+               }, `
+                       create  /dev
+               `},
+       }
+       for _, tt := range tests {
+               tt := tt
+               tt.run(t)
+       }
+}
+
+func TestWatchWrite(t *testing.T) {
+       tests := []testCase{
+               // Files
+               {"truncate file", func(t *testing.T, w *Watcher, tmp string) {
                        file := filepath.Join(tmp, "file")
+                       cat(t, "data", file)
+                       addWatch(t, w, tmp)
+
+                       fp, err := os.Create(file)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       if err := fp.Sync(); err != nil {
+                               t.Fatal(err)
+                       }
+                       eventSeparator()
+                       if _, err := fp.Write([]byte("X")); err != nil {
+                               t.Fatal(err)
+                       }
+                       if err := fp.Close(); err != nil {
+                               t.Fatal(err)
+                       }
+               }, `
+                       write  /file  # truncate
+                       write  /file  # write
 
+                       # Truncate is chmod on kqueue, except NetBSD
+                       netbsd:
+                               write  /file
+                       kqueue:
+                               chmod     /file
+                               write     /file
+               `},
+
+               {"multiple writes to a file", func(t *testing.T, w *Watcher, tmp string) {
+                       file := filepath.Join(tmp, "file")
+                       cat(t, "data", file)
                        addWatch(t, w, tmp)
+
+                       fp, err := os.OpenFile(file, os.O_RDWR, 0)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       if _, err := fp.Write([]byte("X")); err != nil {
+                               t.Fatal(err)
+                       }
+                       if err := fp.Sync(); err != nil {
+                               t.Fatal(err)
+                       }
+                       eventSeparator()
+                       if _, err := fp.Write([]byte("Y")); err != nil {
+                               t.Fatal(err)
+                       }
+                       if err := fp.Close(); err != nil {
+                               t.Fatal(err)
+                       }
+               }, `
+                       write  /file  # write X
+                       write  /file  # write Y
+               `},
+       }
+       for _, tt := range tests {
+               tt := tt
+               tt.run(t)
+       }
+}
+
+func TestWatchRename(t *testing.T) {
+       tests := []testCase{
+               {"rename file in watched dir", func(t *testing.T, w *Watcher, tmp string) {
+                       file := filepath.Join(tmp, "file")
                        cat(t, "asd", file)
+
+                       addWatch(t, w, tmp)
                        mv(t, file, tmp, "renamed")
                }, `
-                       create /file
-                       write  /file
                        rename /file
                        create /renamed
                `},
 
-               {"rename from unwatched directory", func(t *testing.T, w *Watcher, tmp string) {
+               {"rename from unwatched dir", func(t *testing.T, w *Watcher, tmp string) {
                        unwatched := t.TempDir()
 
                        addWatch(t, w, tmp)
@@ -185,7 +331,7 @@ func TestWatchRename(t *testing.T) {
                        create /file
                `},
 
-               {"rename to unwatched directory", func(t *testing.T, w *Watcher, tmp string) {
+               {"rename to unwatched dir", func(t *testing.T, w *Watcher, tmp string) {
                        if runtime.GOOS == "netbsd" && isCI() {
                                t.Skip("fails in CI; see #488")
                        }
@@ -215,14 +361,16 @@ func TestWatchRename(t *testing.T) {
                `},
 
                {"rename overwriting existing file", func(t *testing.T, w *Watcher, tmp string) {
-                       touch(t, tmp, "renamed")
-                       addWatch(t, w, tmp)
-
                        unwatched := t.TempDir()
                        file := filepath.Join(unwatched, "file")
+
+                       touch(t, tmp, "renamed")
                        touch(t, file)
+
+                       addWatch(t, w, tmp)
                        mv(t, file, tmp, "renamed")
                }, `
+                       # TODO: this should really be RENAME.
                        remove /renamed
                        create /renamed
 
@@ -232,7 +380,7 @@ func TestWatchRename(t *testing.T) {
 
                        # TODO: this is broken.
                        dragonfly:
-                REMOVE|WRITE         "/"
+                               REMOVE|WRITE         "/"
                `},
 
                {"rename watched directory", func(t *testing.T, w *Watcher, tmp string) {
@@ -264,6 +412,31 @@ func TestWatchRename(t *testing.T) {
                                CREATE               "/dir-renamed"   # mv
                                REMOVE|RENAME        "/dir"
                `},
+
+               {"rename watched file", func(t *testing.T, w *Watcher, tmp string) {
+                       file := filepath.Join(tmp, "file")
+                       rename := filepath.Join(tmp, "rename-one")
+                       touch(t, file)
+
+                       addWatch(t, w, file)
+
+                       mv(t, file, rename)
+                       mv(t, rename, tmp, "rename-two")
+               }, `
+                       # TODO: this should update the path. And even then, not clear what
+                       # go renamed to what.
+                       rename /file  # mv file rename
+                       rename /file  # mv rename rename-two
+
+                       # TODO: seems to lose the watch?
+                       kqueue:
+                               rename     /file
+
+                       # It's actually more correct on Windows.
+                       windows:
+                               rename     /file
+                               rename     /rename-one
+               `},
        }
 
        for _, tt := range tests {
@@ -282,12 +455,12 @@ func TestWatchSymlink(t *testing.T) {
                        create /link
 
                        windows:
-                create    /link
-                write     /link
+                               create    /link
+                               write     /link
 
                        # No events at all on Dragonfly
                        # TODO: should fix this.
-            dragonfly:
+                       dragonfly:
                                empty
                `},
 
@@ -603,6 +776,10 @@ func TestClose(t *testing.T) {
        })
 
        t.Run("closes channels after read", func(t *testing.T) {
+               if runtime.GOOS == "netbsd" {
+                       t.Skip("flaky") // TODO
+               }
+
                t.Parallel()
 
                tmp := t.TempDir()
@@ -735,17 +912,17 @@ func TestEventString(t *testing.T) {
                in   Event
                want string
        }{
-               {Event{}, `"": `},
-               {Event{"/file", 0}, `"/file": `},
+               {Event{}, `[no events]   ""`},
+               {Event{"/file", 0}, `[no events]   "/file"`},
 
                {Event{"/file", Chmod | Create},
-                       `"/file": CREATE|CHMOD`},
+                       `CREATE|CHMOD  "/file"`},
                {Event{"/file", Rename},
-                       `"/file": RENAME`},
+                       `RENAME        "/file"`},
                {Event{"/file", Remove},
-                       `"/file": REMOVE`},
+                       `REMOVE        "/file"`},
                {Event{"/file", Write | Chmod},
-                       `"/file": WRITE|CHMOD`},
+                       `WRITE|CHMOD   "/file"`},
        }
 
        for _, tt := range tests {
index d71d9b56c31ef0ca358583b5d63746012a35680e..ec1c3bcbc5e96efdd34c04f2183144071b1cad1d 100644 (file)
@@ -11,6 +11,8 @@ import (
        "sync"
        "testing"
        "time"
+
+       "github.com/fsnotify/fsnotify/internal"
 )
 
 type testCase struct {
@@ -165,6 +167,36 @@ func symlink(t *testing.T, target string, link ...string) {
        }
 }
 
+// mkfifo
+func mkfifo(t *testing.T, path ...string) {
+       t.Helper()
+       if len(path) < 1 {
+               t.Fatalf("mkfifo: path must have at least one element: %s", path)
+       }
+       err := internal.Mkfifo(filepath.Join(path...), 0o644)
+       if err != nil {
+               t.Fatalf("mkfifo(%q): %s", filepath.Join(path...), err)
+       }
+       if shouldWait(path...) {
+               eventSeparator()
+       }
+}
+
+// mknod
+func mknod(t *testing.T, dev int, path ...string) {
+       t.Helper()
+       if len(path) < 1 {
+               t.Fatalf("mknod: path must have at least one element: %s", path)
+       }
+       err := internal.Mknod(filepath.Join(path...), 0o644, dev)
+       if err != nil {
+               t.Fatalf("mknod(%d, %q): %s", dev, filepath.Join(path...), err)
+       }
+       if shouldWait(path...) {
+               eventSeparator()
+       }
+}
+
 // cat
 func cat(t *testing.T, data string, path ...string) {
        t.Helper()
index 1d7d68589ec77cf7b360b8690eb25525273cdfcf..6a6d0680df05a78111823c5ffe6c1acc1153c9cb 100644 (file)
@@ -35,4 +35,6 @@ func SetRlimit() {
        }
 }
 
-func Maxfiles() uint64 { return maxfiles }
+func Maxfiles() uint64                              { return maxfiles }
+func Mkfifo(path string, mode uint32) error         { return unix.Mkfifo(path, mode) }
+func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) }
diff --git a/internal/debug_darwin.go b/internal/debug_darwin.go
new file mode 100644 (file)
index 0000000..928319f
--- /dev/null
@@ -0,0 +1,57 @@
+package internal
+
+import "golang.org/x/sys/unix"
+
+var names = []struct {
+       n string
+       m uint32
+}{
+       {"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE},
+       {"NOTE_ATTRIB", unix.NOTE_ATTRIB},
+       {"NOTE_BACKGROUND", unix.NOTE_BACKGROUND},
+       {"NOTE_CHILD", unix.NOTE_CHILD},
+       {"NOTE_CRITICAL", unix.NOTE_CRITICAL},
+       {"NOTE_DELETE", unix.NOTE_DELETE},
+       {"NOTE_EXEC", unix.NOTE_EXEC},
+       {"NOTE_EXIT", unix.NOTE_EXIT},
+       {"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS},
+       {"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR},
+       {"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL},
+       {"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL},
+       {"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK},
+       {"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY},
+       {"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED},
+       {"NOTE_EXTEND", unix.NOTE_EXTEND},
+       {"NOTE_FFAND", unix.NOTE_FFAND},
+       {"NOTE_FFCOPY", unix.NOTE_FFCOPY},
+       {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
+       {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
+       {"NOTE_FFNOP", unix.NOTE_FFNOP},
+       {"NOTE_FFOR", unix.NOTE_FFOR},
+       {"NOTE_FORK", unix.NOTE_FORK},
+       {"NOTE_FUNLOCK", unix.NOTE_FUNLOCK},
+       {"NOTE_LEEWAY", unix.NOTE_LEEWAY},
+       {"NOTE_LINK", unix.NOTE_LINK},
+       {"NOTE_LOWAT", unix.NOTE_LOWAT},
+       {"NOTE_MACHTIME", unix.NOTE_MACHTIME},
+       {"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME},
+       {"NOTE_NONE", unix.NOTE_NONE},
+       {"NOTE_NSECONDS", unix.NOTE_NSECONDS},
+       {"NOTE_OOB", unix.NOTE_OOB},
+       //{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!)
+       {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
+       {"NOTE_REAP", unix.NOTE_REAP},
+       {"NOTE_RENAME", unix.NOTE_RENAME},
+       {"NOTE_REVOKE", unix.NOTE_REVOKE},
+       {"NOTE_SECONDS", unix.NOTE_SECONDS},
+       {"NOTE_SIGNAL", unix.NOTE_SIGNAL},
+       {"NOTE_TRACK", unix.NOTE_TRACK},
+       {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
+       {"NOTE_TRIGGER", unix.NOTE_TRIGGER},
+       {"NOTE_USECONDS", unix.NOTE_USECONDS},
+       {"NOTE_VM_ERROR", unix.NOTE_VM_ERROR},
+       {"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE},
+       {"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE},
+       {"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE},
+       {"NOTE_WRITE", unix.NOTE_WRITE},
+}
diff --git a/internal/debug_dragonfly.go b/internal/debug_dragonfly.go
new file mode 100644 (file)
index 0000000..3186b0c
--- /dev/null
@@ -0,0 +1,33 @@
+package internal
+
+import "golang.org/x/sys/unix"
+
+var names = []struct {
+       n string
+       m uint32
+}{
+       {"NOTE_ATTRIB", unix.NOTE_ATTRIB},
+       {"NOTE_CHILD", unix.NOTE_CHILD},
+       {"NOTE_DELETE", unix.NOTE_DELETE},
+       {"NOTE_EXEC", unix.NOTE_EXEC},
+       {"NOTE_EXIT", unix.NOTE_EXIT},
+       {"NOTE_EXTEND", unix.NOTE_EXTEND},
+       {"NOTE_FFAND", unix.NOTE_FFAND},
+       {"NOTE_FFCOPY", unix.NOTE_FFCOPY},
+       {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
+       {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
+       {"NOTE_FFNOP", unix.NOTE_FFNOP},
+       {"NOTE_FFOR", unix.NOTE_FFOR},
+       {"NOTE_FORK", unix.NOTE_FORK},
+       {"NOTE_LINK", unix.NOTE_LINK},
+       {"NOTE_LOWAT", unix.NOTE_LOWAT},
+       {"NOTE_OOB", unix.NOTE_OOB},
+       {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
+       {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
+       {"NOTE_RENAME", unix.NOTE_RENAME},
+       {"NOTE_REVOKE", unix.NOTE_REVOKE},
+       {"NOTE_TRACK", unix.NOTE_TRACK},
+       {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
+       {"NOTE_TRIGGER", unix.NOTE_TRIGGER},
+       {"NOTE_WRITE", unix.NOTE_WRITE},
+}
diff --git a/internal/debug_freebsd.go b/internal/debug_freebsd.go
new file mode 100644 (file)
index 0000000..f69fdb9
--- /dev/null
@@ -0,0 +1,42 @@
+package internal
+
+import "golang.org/x/sys/unix"
+
+var names = []struct {
+       n string
+       m uint32
+}{
+       {"NOTE_ABSTIME", unix.NOTE_ABSTIME},
+       {"NOTE_ATTRIB", unix.NOTE_ATTRIB},
+       {"NOTE_CHILD", unix.NOTE_CHILD},
+       {"NOTE_CLOSE", unix.NOTE_CLOSE},
+       {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE},
+       {"NOTE_DELETE", unix.NOTE_DELETE},
+       {"NOTE_EXEC", unix.NOTE_EXEC},
+       {"NOTE_EXIT", unix.NOTE_EXIT},
+       {"NOTE_EXTEND", unix.NOTE_EXTEND},
+       {"NOTE_FFAND", unix.NOTE_FFAND},
+       {"NOTE_FFCOPY", unix.NOTE_FFCOPY},
+       {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
+       {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
+       {"NOTE_FFNOP", unix.NOTE_FFNOP},
+       {"NOTE_FFOR", unix.NOTE_FFOR},
+       {"NOTE_FILE_POLL", unix.NOTE_FILE_POLL},
+       {"NOTE_FORK", unix.NOTE_FORK},
+       {"NOTE_LINK", unix.NOTE_LINK},
+       {"NOTE_LOWAT", unix.NOTE_LOWAT},
+       {"NOTE_MSECONDS", unix.NOTE_MSECONDS},
+       {"NOTE_NSECONDS", unix.NOTE_NSECONDS},
+       {"NOTE_OPEN", unix.NOTE_OPEN},
+       {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
+       {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
+       {"NOTE_READ", unix.NOTE_READ},
+       {"NOTE_RENAME", unix.NOTE_RENAME},
+       {"NOTE_REVOKE", unix.NOTE_REVOKE},
+       {"NOTE_SECONDS", unix.NOTE_SECONDS},
+       {"NOTE_TRACK", unix.NOTE_TRACK},
+       {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
+       {"NOTE_TRIGGER", unix.NOTE_TRIGGER},
+       {"NOTE_USECONDS", unix.NOTE_USECONDS},
+       {"NOTE_WRITE", unix.NOTE_WRITE},
+}
diff --git a/internal/debug_kqueue.go b/internal/debug_kqueue.go
new file mode 100644 (file)
index 0000000..47f7660
--- /dev/null
@@ -0,0 +1,27 @@
+//go:build freebsd || openbsd || netbsd || dragonfly || darwin
+// +build freebsd openbsd netbsd dragonfly darwin
+
+package internal
+
+import (
+       "fmt"
+       "os"
+       "strings"
+       "time"
+
+       "golang.org/x/sys/unix"
+)
+
+func Debug(name string, kevent *unix.Kevent_t) {
+       mask := uint32(kevent.Fflags)
+       var l []string
+       for _, n := range names {
+               if mask&n.m == n.m {
+                       l = append(l, n.n)
+               }
+       }
+
+       fmt.Fprintf(os.Stderr, "%s  %-20s → %s\n",
+               time.Now().Format("15:04:05.0000"),
+               strings.Join(l, " | "), name)
+}
diff --git a/internal/debug_linux.go b/internal/debug_linux.go
new file mode 100644 (file)
index 0000000..86613b9
--- /dev/null
@@ -0,0 +1,63 @@
+package internal
+
+import (
+       "fmt"
+       "os"
+       "strings"
+       "time"
+
+       "golang.org/x/sys/unix"
+)
+
+func Debug(name string, mask uint32) {
+       names := []struct {
+               n string
+               m uint32
+       }{
+               {"IN_ACCESS", unix.IN_ACCESS},
+               {"IN_ALL_EVENTS", unix.IN_ALL_EVENTS},
+               {"IN_ATTRIB", unix.IN_ATTRIB},
+               {"IN_CLASSA_HOST", unix.IN_CLASSA_HOST},
+               {"IN_CLASSA_MAX", unix.IN_CLASSA_MAX},
+               {"IN_CLASSA_NET", unix.IN_CLASSA_NET},
+               {"IN_CLASSA_NSHIFT", unix.IN_CLASSA_NSHIFT},
+               {"IN_CLASSB_HOST", unix.IN_CLASSB_HOST},
+               {"IN_CLASSB_MAX", unix.IN_CLASSB_MAX},
+               {"IN_CLASSB_NET", unix.IN_CLASSB_NET},
+               {"IN_CLASSB_NSHIFT", unix.IN_CLASSB_NSHIFT},
+               {"IN_CLASSC_HOST", unix.IN_CLASSC_HOST},
+               {"IN_CLASSC_NET", unix.IN_CLASSC_NET},
+               {"IN_CLASSC_NSHIFT", unix.IN_CLASSC_NSHIFT},
+               {"IN_CLOSE", unix.IN_CLOSE},
+               {"IN_CLOSE_NOWRITE", unix.IN_CLOSE_NOWRITE},
+               {"IN_CLOSE_WRITE", unix.IN_CLOSE_WRITE},
+               {"IN_CREATE", unix.IN_CREATE},
+               {"IN_DELETE", unix.IN_DELETE},
+               {"IN_DELETE_SELF", unix.IN_DELETE_SELF},
+               {"IN_DONT_FOLLOW", unix.IN_DONT_FOLLOW},
+               {"IN_EXCL_UNLINK", unix.IN_EXCL_UNLINK},
+               {"IN_IGNORED", unix.IN_IGNORED},
+               {"IN_ISDIR", unix.IN_ISDIR},
+               {"IN_LOOPBACKNET", unix.IN_LOOPBACKNET},
+               {"IN_MASK_ADD", unix.IN_MASK_ADD},
+               {"IN_MASK_CREATE", unix.IN_MASK_CREATE},
+               {"IN_MODIFY", unix.IN_MODIFY},
+               {"IN_MOVE", unix.IN_MOVE},
+               {"IN_MOVED_FROM", unix.IN_MOVED_FROM},
+               {"IN_MOVED_TO", unix.IN_MOVED_TO},
+               {"IN_MOVE_SELF", unix.IN_MOVE_SELF},
+               {"IN_ONESHOT", unix.IN_ONESHOT},
+               {"IN_ONLYDIR", unix.IN_ONLYDIR},
+               {"IN_OPEN", unix.IN_OPEN},
+               {"IN_Q_OVERFLOW", unix.IN_Q_OVERFLOW},
+               {"IN_UNMOUNT", unix.IN_UNMOUNT},
+       }
+
+       var l []string
+       for _, n := range names {
+               if mask&n.m == n.m {
+                       l = append(l, n.n)
+               }
+       }
+       fmt.Fprintf(os.Stderr, "%s  %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name)
+}
diff --git a/internal/debug_netbsd.go b/internal/debug_netbsd.go
new file mode 100644 (file)
index 0000000..e5b3b6f
--- /dev/null
@@ -0,0 +1,25 @@
+package internal
+
+import "golang.org/x/sys/unix"
+
+var names = []struct {
+       n string
+       m uint32
+}{
+       {"NOTE_ATTRIB", unix.NOTE_ATTRIB},
+       {"NOTE_CHILD", unix.NOTE_CHILD},
+       {"NOTE_DELETE", unix.NOTE_DELETE},
+       {"NOTE_EXEC", unix.NOTE_EXEC},
+       {"NOTE_EXIT", unix.NOTE_EXIT},
+       {"NOTE_EXTEND", unix.NOTE_EXTEND},
+       {"NOTE_FORK", unix.NOTE_FORK},
+       {"NOTE_LINK", unix.NOTE_LINK},
+       {"NOTE_LOWAT", unix.NOTE_LOWAT},
+       {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
+       {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
+       {"NOTE_RENAME", unix.NOTE_RENAME},
+       {"NOTE_REVOKE", unix.NOTE_REVOKE},
+       {"NOTE_TRACK", unix.NOTE_TRACK},
+       {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
+       {"NOTE_WRITE", unix.NOTE_WRITE},
+}
diff --git a/internal/debug_openbsd.go b/internal/debug_openbsd.go
new file mode 100644 (file)
index 0000000..996b441
--- /dev/null
@@ -0,0 +1,28 @@
+package internal
+
+import "golang.org/x/sys/unix"
+
+var names = []struct {
+       n string
+       m uint32
+}{
+       {"NOTE_ATTRIB", unix.NOTE_ATTRIB},
+       {"NOTE_CHANGE", unix.NOTE_CHANGE},
+       {"NOTE_CHILD", unix.NOTE_CHILD},
+       {"NOTE_DELETE", unix.NOTE_DELETE},
+       {"NOTE_EOF", unix.NOTE_EOF},
+       {"NOTE_EXEC", unix.NOTE_EXEC},
+       {"NOTE_EXIT", unix.NOTE_EXIT},
+       {"NOTE_EXTEND", unix.NOTE_EXTEND},
+       {"NOTE_FORK", unix.NOTE_FORK},
+       {"NOTE_LINK", unix.NOTE_LINK},
+       {"NOTE_LOWAT", unix.NOTE_LOWAT},
+       {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
+       {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
+       {"NOTE_RENAME", unix.NOTE_RENAME},
+       {"NOTE_REVOKE", unix.NOTE_REVOKE},
+       {"NOTE_TRACK", unix.NOTE_TRACK},
+       {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
+       {"NOTE_TRUNCATE", unix.NOTE_TRUNCATE},
+       {"NOTE_WRITE", unix.NOTE_WRITE},
+}
diff --git a/internal/debug_windows.go b/internal/debug_windows.go
new file mode 100644 (file)
index 0000000..50b117f
--- /dev/null
@@ -0,0 +1,39 @@
+package internal
+
+import (
+       "fmt"
+       "os"
+       "strings"
+       "time"
+
+       "golang.org/x/sys/windows"
+)
+
+func Debug(name string, mask uint32) {
+       names := []struct {
+               n string
+               m uint32
+       }{
+               //{"FILE_NOTIFY_CHANGE_FILE_NAME", windows.FILE_NOTIFY_CHANGE_FILE_NAME},
+               //{"FILE_NOTIFY_CHANGE_DIR_NAME", windows.FILE_NOTIFY_CHANGE_DIR_NAME},
+               //{"FILE_NOTIFY_CHANGE_ATTRIBUTES", windows.FILE_NOTIFY_CHANGE_ATTRIBUTES},
+               //{"FILE_NOTIFY_CHANGE_SIZE", windows.FILE_NOTIFY_CHANGE_SIZE},
+               //{"FILE_NOTIFY_CHANGE_LAST_WRITE", windows.FILE_NOTIFY_CHANGE_LAST_WRITE},
+               //{"FILE_NOTIFY_CHANGE_LAST_ACCESS", windows.FILE_NOTIFY_CHANGE_LAST_ACCESS},
+               //{"FILE_NOTIFY_CHANGE_CREATION", windows.FILE_NOTIFY_CHANGE_CREATION},
+               //{"FILE_NOTIFY_CHANGE_SECURITY", windows.FILE_NOTIFY_CHANGE_SECURITY},
+               {"FILE_ACTION_ADDED", windows.FILE_ACTION_ADDED},
+               {"FILE_ACTION_REMOVED", windows.FILE_ACTION_REMOVED},
+               {"FILE_ACTION_MODIFIED", windows.FILE_ACTION_MODIFIED},
+               {"FILE_ACTION_RENAMED_OLD_NAME", windows.FILE_ACTION_RENAMED_OLD_NAME},
+               {"FILE_ACTION_RENAMED_NEW_NAME", windows.FILE_ACTION_RENAMED_NEW_NAME},
+       }
+
+       var l []string
+       for _, n := range names {
+               if mask&n.m == n.m {
+                       l = append(l, n.n)
+               }
+       }
+       fmt.Fprintf(os.Stderr, "%s  %-20s → %s\n", time.Now().Format("15:04:05.0000"), strings.Join(l, " | "), name)
+}
diff --git a/internal/freebsd.go b/internal/freebsd.go
new file mode 100644 (file)
index 0000000..4128482
--- /dev/null
@@ -0,0 +1,32 @@
+//go:build freebsd
+// +build freebsd
+
+package internal
+
+import (
+       "syscall"
+
+       "golang.org/x/sys/unix"
+)
+
+var (
+       SyscallEACCES = syscall.EACCES
+       UnixEACCES    = unix.EACCES
+)
+
+var maxfiles uint64
+
+// Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/
+func SetRlimit() {
+       var l syscall.Rlimit
+       err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l)
+       if err == nil && l.Cur != l.Max {
+               l.Cur = l.Max
+               syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l)
+       }
+       maxfiles = uint64(l.Cur)
+}
+
+func Maxfiles() uint64                              { return maxfiles }
+func Mkfifo(path string, mode uint32) error         { return unix.Mkfifo(path, mode) }
+func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) }
index dd638ec8faed7ba641fc91a47b2fd16f16f46624..301b242e6ebe3c09f6599c75c0f2bf5582ef5166 100644 (file)
@@ -1,5 +1,5 @@
-//go:build !windows && !darwin
-// +build !windows,!darwin
+//go:build !windows && !darwin && !freebsd
+// +build !windows,!darwin,!freebsd
 
 package internal
 
@@ -27,4 +27,6 @@ func SetRlimit() {
        maxfiles = uint64(l.Cur)
 }
 
-func Maxfiles() uint64 { return maxfiles }
+func Maxfiles() uint64                              { return maxfiles }
+func Mkfifo(path string, mode uint32) error         { return unix.Mkfifo(path, mode) }
+func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) }
index b0d5ae7769380950deff7682c0a541bfa348fcdb..0e9b3b23869e4583cd34a367cb28a8f2144aa15d 100644 (file)
@@ -13,6 +13,7 @@ var (
        UnixEACCES    = errors.New("dummy")
 )
 
-func SetRlimit() {}
-
-func Maxfiles() uint64 { return 1<<64 - 1 }
+func SetRlimit()                                    {}
+func Maxfiles() uint64                              { return 1<<64 - 1 }
+func Mkfifo(path string, mode uint32) error         { return errors.New("no FIFOs on Windows") }
+func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") }
diff --git a/mkdoc.zsh b/mkdoc.zsh
new file mode 100755 (executable)
index 0000000..fd44b25
--- /dev/null
+++ b/mkdoc.zsh
@@ -0,0 +1,198 @@
+#!/usr/bin/env zsh
+[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
+setopt err_exit no_unset pipefail extended_glob
+
+# Simple script to update the godoc comments on all watchers. Probably took me
+# more time to write this than doing it manually, but ah well 🙃
+
+watcher=$(<<EOF
+// Watcher watches a set of files, delivering events to a channel.
+//
+// A watcher should not be copied (e.g. pass it by pointer, rather than by
+// value).
+//
+// # Linux notes
+//
+// When a file is removed a Remove event won't be emitted until all file
+// descriptors are closed, and deletes will always emit a Chmod. For example:
+//
+//     fp := os.Open("file")
+//     os.Remove("file")        // Triggers Chmod
+//     fp.Close()               // Triggers Remove
+//
+// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
+// for the number of watches per user, and fs.inotify.max_user_instances
+// specifies the maximum number of inotify instances per user. Every Watcher you
+// create is an "instance", and every path you add is a "watch".
+//
+// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
+// /proc/sys/fs/inotify/max_user_instances
+//
+// To increase them you can use sysctl or write the value to the /proc file:
+//
+//     # Default values on Linux 5.18
+//     sysctl fs.inotify.max_user_watches=124983
+//     sysctl fs.inotify.max_user_instances=128
+//
+// To make the changes persist on reboot edit /etc/sysctl.conf or
+// /usr/lib/sysctl.d/50-default.conf (on some systemd systems):
+//
+//     fs.inotify.max_user_watches=124983
+//     fs.inotify.max_user_instances=128
+//
+// Reaching the limit will result in a "no space left on device" or "too many open
+// files" error.
+//
+// # kqueue notes (macOS, BSD)
+//
+// kqueue requires opening a file descriptor for every file that's being watched;
+// so if you're watching a directory with five files then that's six file
+// descriptors. You will run in to your system's "max open files" limit faster on
+// these platforms.
+//
+// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
+// control the maximum number of open files, as well as /etc/login.conf on BSD
+// systems.
+//
+// # macOS notes
+//
+// Spotlight indexing on macOS can result in multiple events (see [#15]). A
+// temporary workaround is to add your folder(s) to the "Spotlight Privacy
+// Settings" until we have a native FSEvents implementation (see [#11]).
+//
+// [#11]: https://github.com/fsnotify/fsnotify/issues/11
+// [#15]: https://github.com/fsnotify/fsnotify/issues/15
+EOF
+)
+
+new=$(<<EOF
+// NewWatcher creates a new Watcher.
+EOF
+)
+
+add=$(<<EOF
+// Add starts monitoring the path for changes.
+//
+// A path can only be watched once; attempting to watch it more than once will
+// return an error. Paths that do not yet exist on the filesystem cannot be
+// added. A watch will be automatically removed if the path is deleted.
+//
+// A path will remain watched if it gets renamed to somewhere else on the same
+// filesystem, but the monitor will get removed if the path gets deleted and
+// re-created.
+//
+// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
+// filesystems (/proc, /sys, etc.) generally don't work.
+//
+// # Watching directories
+//
+// All files in a directory are monitored, including new files that are created
+// after the watcher is started. Subdirectories are not watched (i.e. it's
+// non-recursive).
+//
+// # Watching files
+//
+// Watching individual files (rather than directories) is generally not
+// recommended as many tools update files atomically. Instead of "just" writing
+// to the file a temporary file will be written to first, and if successful the
+// temporary file is moved to to destination, removing the original, or some
+// variant thereof. The watcher on the original file is now lost, as it no
+// longer exists.
+//
+// Instead, watch the parent directory and use [Event.Name] to filter out files
+// you're not interested in. There is an example of this in cmd/fsnotify/file.go
+EOF
+)
+
+remove=$(<<EOF
+// Remove stops monitoring the path for changes.
+//
+// Directories are always removed non-recursively. For example, if you added
+// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
+//
+// Removing a path that has not yet been added returns [ErrNonExistentWatch].
+EOF
+)
+
+close=$(<<EOF
+// Close removes all watches and closes the events channel.
+EOF
+)
+
+events=$(<<EOF
+       // Events sends the filesystem change events.
+       //
+       // fsnotify can send the following events; a "path" here can refer to a
+       // file, directory, symbolic link, or special files like a FIFO.
+       //
+       //   fsnotify.Create    A new path was created; this may be followed by one
+       //                      or more Write events if data also gets written to a
+       //                      file.
+       //
+       //   fsnotify.Remove    A path was removed.
+       //
+       //   fsnotify.Rename    A path was renamed. A rename is always sent with the
+       //                      old path as [Event.Name], and a Create event will be
+       //                      sent with the new name. Renames are only sent for
+       //                      paths that are currently watched; e.g. moving an
+       //                      unmonitored file into a monitored directory will
+       //                      show up as just a Create. Similarly, renaming a file
+       //                      to outside a monitored directory will show up as
+       //                      only a Rename.
+       //
+       //   fsnotify.Write     A file or named pipe was written to. A Truncate will
+       //                      also trigger a Write. A single "write action"
+       //                      initiated by the user may show up as one or multiple
+       //                      writes, depending on when the system syncs things to
+       //                      disk. For example when compiling a large Go program
+       //                      you may get hundreds of Write events, so you
+       //                      probably want to wait until you've stopped receiving
+       //                      them (see the dedup example in cmd/fsnotify).
+       //
+       //   fsnotify.Chmod     Attributes were changes (never sent on Windows). On
+       //                      Linux this is also sent when a file is removed (or
+       //                      more accurately, when a link to an inode is
+       //                      removed), and on kqueue when a file is truncated.
+EOF
+)
+
+errors=$(<<EOF
+       // Errors sends any errors.
+EOF
+)
+
+set-cmt() {
+       local pat=$1
+       local cmt=$2
+
+       IFS=$'\n' local files=($(grep -n $pat backend_*~*_test.go))
+       for f in $files; do
+               IFS=':' local fields=($=f)
+               local file=$fields[1]
+               local end=$(( $fields[2] - 1 ))
+
+               # Find start of comment.
+               local start=0
+               IFS=$'\n' local lines=($(head -n$end $file))
+               for (( i = 1; i <= $#lines; i++ )); do
+                       local line=$lines[-$i]
+                       if ! grep -q '^[[:space:]]*//' <<<$line; then
+                               start=$(( end - (i - 2) ))
+                               break
+                       fi
+               done
+
+               head -n $(( start - 1 )) $file  >/tmp/x
+               print -r -- $cmt                >>/tmp/x
+               tail -n+$(( end + 1 ))   $file  >>/tmp/x
+               mv /tmp/x $file
+       done
+}
+
+set-cmt '^type Watcher struct '             $watcher
+set-cmt '^func NewWatcher('                 $new
+set-cmt '^func (w \*Watcher) Add('          $add
+set-cmt '^func (w \*Watcher) Remove('       $remove
+set-cmt '^func (w \*Watcher) Close('        $close
+set-cmt '^[[:space:]]*Events *chan Event$'  $events
+set-cmt '^[[:space:]]*Errors *chan error$'  $errors