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
---
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`
)
// 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")
}
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
}
)
// 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)
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
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() {
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)
)
// 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)).
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 {
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{}{}
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()
// 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)
}
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
}
)
// 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
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 {
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 {
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,
--- /dev/null
+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)
+ }
+ }
+}
--- /dev/null
+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)
+ }
+ }
+}
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...)
+ }
}
--- /dev/null
+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)
+ }
+ }
+}
"strings"
)
-// Event represents a single file system notification.
+// Event represents a file system notification.
type Event struct {
// Path to the file or directory.
//
// 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
b.WriteString("|CHMOD")
}
if b.Len() == 0 {
- return ""
+ return "[no events]"
}
return b.String()[1:]
}
// 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)
}
# 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:
# 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) {
}
}
-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)
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")
}
`},
{"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
# TODO: this is broken.
dragonfly:
- REMOVE|WRITE "/"
+ REMOVE|WRITE "/"
`},
{"rename watched directory", func(t *testing.T, w *Watcher, tmp string) {
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 {
create /link
windows:
- create /link
- write /link
+ create /link
+ write /link
# No events at all on Dragonfly
# TODO: should fix this.
- dragonfly:
+ dragonfly:
empty
`},
})
t.Run("closes channels after read", func(t *testing.T) {
+ if runtime.GOOS == "netbsd" {
+ t.Skip("flaky") // TODO
+ }
+
t.Parallel()
tmp := t.TempDir()
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 {
"sync"
"testing"
"time"
+
+ "github.com/fsnotify/fsnotify/internal"
)
type testCase struct {
}
}
+// 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()
}
}
-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) }
--- /dev/null
+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},
+}
--- /dev/null
+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},
+}
--- /dev/null
+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},
+}
--- /dev/null
+//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)
+}
--- /dev/null
+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)
+}
--- /dev/null
+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},
+}
--- /dev/null
+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},
+}
--- /dev/null
+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)
+}
--- /dev/null
+//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)) }
-//go:build !windows && !darwin
-// +build !windows,!darwin
+//go:build !windows && !darwin && !freebsd
+// +build !windows,!darwin,!freebsd
package internal
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) }
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") }
--- /dev/null
+#!/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