| // Package fsnotify provides a cross-platform interface for file system |
| // notifications. |
| // |
| // Currently supported systems: |
| // |
| // - Linux via inotify |
| // - BSD, macOS via kqueue |
| // - Windows via ReadDirectoryChangesW |
| // - illumos via FEN |
| // |
| // # FSNOTIFY_DEBUG |
| // |
| // Set the FSNOTIFY_DEBUG environment variable to "1" to print debug messages to |
| // stderr. This can be useful to track down some problems, especially in cases |
| // where fsnotify is used as an indirect dependency. |
| // |
| // Every event will be printed as soon as there's something useful to print, |
| // with as little processing from fsnotify. |
| // |
| // Example output: |
| // |
| // FSNOTIFY_DEBUG: 11:34:23.633087586 256:IN_CREATE → "/tmp/file-1" |
| // FSNOTIFY_DEBUG: 11:34:23.633202319 4:IN_ATTRIB → "/tmp/file-1" |
| // FSNOTIFY_DEBUG: 11:34:28.989728764 512:IN_DELETE → "/tmp/file-1" |
| package fsnotify |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| ) |
| |
| // Watcher watches a set of paths, delivering events on 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 |
| // |
| // This is the event that inotify sends, so not much can be changed about this. |
| // |
| // 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 (details differ per Linux distro; check |
| // your distro's documentation): |
| // |
| // 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. |
| // |
| // # Windows notes |
| // |
| // Paths can be added as "C:\\path\\to\\dir", but forward slashes |
| // ("C:/path/to/dir") will also work. |
| // |
| // When a watched directory is removed it will always send an event for the |
| // directory itself, but may not send events for all files in that directory. |
| // Sometimes it will send events for all files, sometimes it will send no |
| // events, and often only for some files. |
| // |
| // The default ReadDirectoryChangesW() buffer size is 64K, which is the largest |
| // value that is guaranteed to work with SMB filesystems. If you have many |
| // events in quick succession this may not be enough, and you will have to use |
| // [WithBufferSize] to increase the value. |
| type Watcher struct { |
| b backend |
| |
| // 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 file 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, and you may |
| // want to wait until you've stopped receiving them |
| // (see the dedup example in cmd/fsnotify). |
| // |
| // Some systems may send Write event for directories |
| // when the directory content changes. |
| // |
| // fsnotify.Chmod Attributes were changed. On Linux this is also sent |
| // when a file is removed (or more accurately, when a |
| // link to an inode is removed). On kqueue it's sent |
| // when a file is truncated. On Windows it's never |
| // sent. |
| Events chan Event |
| |
| // Errors sends any errors. |
| Errors chan error |
| } |
| |
| // Event represents a file system notification. |
| type Event struct { |
| // Path to the file or directory. |
| // |
| // Paths are relative to the input; for example with Add("dir") the Name |
| // will be set to "dir/file" if you create that file, but if you use |
| // Add("/path/to/dir") it will be "/path/to/dir/file". |
| Name string |
| |
| // File operation that triggered the event. |
| // |
| // This is a bitmask and some systems may send multiple operations at once. |
| // Use the Event.Has() method instead of comparing with ==. |
| Op Op |
| |
| // Create events will have this set to the old path if it's a rename. This |
| // only works when both the source and destination are watched. It's not |
| // reliable when watching individual files, only directories. |
| // |
| // For example "mv /tmp/file /tmp/rename" will emit: |
| // |
| // Event{Op: Rename, Name: "/tmp/file"} |
| // Event{Op: Create, Name: "/tmp/rename", RenamedFrom: "/tmp/file"} |
| renamedFrom string |
| } |
| |
| // Op describes a set of file operations. |
| type Op uint32 |
| |
| // The operations fsnotify can trigger; see the documentation on [Watcher] for a |
| // full description, and check them with [Event.Has]. |
| const ( |
| // A new pathname was created. |
| Create Op = 1 << iota |
| |
| // The pathname was written to; this does *not* mean the write has finished, |
| // and a write can be followed by more writes. |
| Write |
| |
| // The path was removed; any watches on it will be removed. Some "remove" |
| // operations may trigger a Rename if the file is actually moved (for |
| // example "remove to trash" is often a rename). |
| Remove |
| |
| // The path was renamed to something else; any watches on it will be |
| // removed. |
| Rename |
| |
| // File attributes were changed. |
| // |
| // It's generally not recommended to take action on this event, as it may |
| // get triggered very frequently by some software. For example, Spotlight |
| // indexing on macOS, anti-virus software, backup software, etc. |
| Chmod |
| |
| // File descriptor was opened. |
| // |
| // Only works on Linux and FreeBSD. |
| xUnportableOpen |
| |
| // File was read from. |
| // |
| // Only works on Linux and FreeBSD. |
| xUnportableRead |
| |
| // File opened for writing was closed. |
| // |
| // Only works on Linux and FreeBSD. |
| // |
| // The advantage of using this over Write is that it's more reliable than |
| // waiting for Write events to stop. It's also faster (if you're not |
| // listening to Write events): copying a file of a few GB can easily |
| // generate tens of thousands of Write events in a short span of time. |
| xUnportableCloseWrite |
| |
| // File opened for reading was closed. |
| // |
| // Only works on Linux and FreeBSD. |
| xUnportableCloseRead |
| ) |
| |
| var ( |
| // ErrNonExistentWatch is used when Remove() is called on a path that's not |
| // added. |
| ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch") |
| |
| // ErrClosed is used when trying to operate on a closed Watcher. |
| ErrClosed = errors.New("fsnotify: watcher already closed") |
| |
| // ErrEventOverflow is reported from the Errors channel when there are too |
| // many events: |
| // |
| // - inotify: inotify returns IN_Q_OVERFLOW – because there are too |
| // many queued events (the fs.inotify.max_queued_events |
| // sysctl can be used to increase this). |
| // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. |
| // - kqueue, fen: Not used. |
| ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow") |
| |
| // ErrUnsupported is returned by AddWith() when WithOps() specified an |
| // Unportable event that's not supported on this platform. |
| xErrUnsupported = errors.New("fsnotify: not supported with this backend") |
| ) |
| |
| // NewWatcher creates a new Watcher. |
| func NewWatcher() (*Watcher, error) { |
| ev, errs := make(chan Event), make(chan error) |
| b, err := newBackend(ev, errs) |
| if err != nil { |
| return nil, err |
| } |
| return &Watcher{b: b, Events: ev, Errors: errs}, nil |
| } |
| |
| // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events |
| // channel. |
| // |
| // The main use case for this is situations with a very large number of events |
| // where the kernel buffer size can't be increased (e.g. due to lack of |
| // permissions). An unbuffered Watcher will perform better for almost all use |
| // cases, and whenever possible you will be better off increasing the kernel |
| // buffers instead of adding a large userspace buffer. |
| func NewBufferedWatcher(sz uint) (*Watcher, error) { |
| ev, errs := make(chan Event), make(chan error) |
| b, err := newBufferedBackend(sz, ev, errs) |
| if err != nil { |
| return nil, err |
| } |
| return &Watcher{b: b, Events: ev, Errors: errs}, nil |
| } |
| |
| // Add starts monitoring the path for changes. |
| // |
| // A path can only be watched once; watching it more than once is a no-op and will |
| // not return an error. Paths that do not yet exist on the filesystem cannot be |
| // watched. |
| // |
| // A watch will be automatically removed if the watched path is deleted or |
| // renamed. The exception is the Windows backend, which doesn't remove the |
| // watcher on renames. |
| // |
| // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special |
| // filesystems (/proc, /sys, etc.) generally don't work. |
| // |
| // Returns [ErrClosed] if [Watcher.Close] was called. |
| // |
| // See [Watcher.AddWith] for a version that allows adding options. |
| // |
| // # 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 programs (especially editors) update files atomically: it |
| // will write to a temporary file which is then moved to to destination, |
| // overwriting the original (or some variant thereof). The watcher on the |
| // original file is now lost, as that no longer exists. |
| // |
| // The upshot of this is that a power failure or crash won't leave a |
| // half-written file. |
| // |
| // 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(path string) error { return w.b.Add(path) } |
| |
| // AddWith is like [Watcher.Add], but allows adding options. When using Add() |
| // the defaults described below are used. |
| // |
| // Possible options are: |
| // |
| // - [WithBufferSize] sets the buffer size for the Windows backend; no-op on |
| // other platforms. The default is 64K (65536 bytes). |
| func (w *Watcher) AddWith(path string, opts ...addOpt) error { return w.b.AddWith(path, opts...) } |
| |
| // 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]. |
| // |
| // Returns nil if [Watcher.Close] was called. |
| func (w *Watcher) Remove(path string) error { return w.b.Remove(path) } |
| |
| // Close removes all watches and closes the Events channel. |
| func (w *Watcher) Close() error { return w.b.Close() } |
| |
| // WatchList returns all paths explicitly added with [Watcher.Add] (and are not |
| // yet removed). |
| // |
| // Returns nil if [Watcher.Close] was called. |
| func (w *Watcher) WatchList() []string { return w.b.WatchList() } |
| |
| // Supports reports if all the listed operations are supported by this platform. |
| // |
| // Create, Write, Remove, Rename, and Chmod are always supported. It can only |
| // return false for an Op starting with Unportable. |
| func (w *Watcher) xSupports(op Op) bool { return w.b.xSupports(op) } |
| |
| func (o Op) String() string { |
| var b strings.Builder |
| if o.Has(Create) { |
| b.WriteString("|CREATE") |
| } |
| if o.Has(Remove) { |
| b.WriteString("|REMOVE") |
| } |
| if o.Has(Write) { |
| b.WriteString("|WRITE") |
| } |
| if o.Has(xUnportableOpen) { |
| b.WriteString("|OPEN") |
| } |
| if o.Has(xUnportableRead) { |
| b.WriteString("|READ") |
| } |
| if o.Has(xUnportableCloseWrite) { |
| b.WriteString("|CLOSE_WRITE") |
| } |
| if o.Has(xUnportableCloseRead) { |
| b.WriteString("|CLOSE_READ") |
| } |
| if o.Has(Rename) { |
| b.WriteString("|RENAME") |
| } |
| if o.Has(Chmod) { |
| b.WriteString("|CHMOD") |
| } |
| if b.Len() == 0 { |
| return "[no events]" |
| } |
| return b.String()[1:] |
| } |
| |
| // Has reports if this operation has the given operation. |
| func (o Op) Has(h Op) bool { return o&h != 0 } |
| |
| // 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 with their path. |
| func (e Event) String() string { |
| if e.renamedFrom != "" { |
| return fmt.Sprintf("%-13s %q ← %q", e.Op.String(), e.Name, e.renamedFrom) |
| } |
| return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name) |
| } |
| |
| type ( |
| backend interface { |
| Add(string) error |
| AddWith(string, ...addOpt) error |
| Remove(string) error |
| WatchList() []string |
| Close() error |
| xSupports(Op) bool |
| } |
| addOpt func(opt *withOpts) |
| withOpts struct { |
| bufsize int |
| op Op |
| noFollow bool |
| sendCreate bool |
| } |
| ) |
| |
| var debug = func() bool { |
| // Check for exactly "1" (rather than mere existence) so we can add |
| // options/flags in the future. I don't know if we ever want that, but it's |
| // nice to leave the option open. |
| return os.Getenv("FSNOTIFY_DEBUG") == "1" |
| }() |
| |
| var defaultOpts = withOpts{ |
| bufsize: 65536, // 64K |
| op: Create | Write | Remove | Rename | Chmod, |
| } |
| |
| func getOptions(opts ...addOpt) withOpts { |
| with := defaultOpts |
| for _, o := range opts { |
| if o != nil { |
| o(&with) |
| } |
| } |
| return with |
| } |
| |
| // WithBufferSize sets the [ReadDirectoryChangesW] buffer size. |
| // |
| // This only has effect on Windows systems, and is a no-op for other backends. |
| // |
| // The default value is 64K (65536 bytes) which is the highest value that works |
| // on all filesystems and should be enough for most applications, but if you |
| // have a large burst of events it may not be enough. You can increase it if |
| // you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]). |
| // |
| // [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw |
| func WithBufferSize(bytes int) addOpt { |
| return func(opt *withOpts) { opt.bufsize = bytes } |
| } |
| |
| // WithOps sets which operations to listen for. The default is [Create], |
| // [Write], [Remove], [Rename], and [Chmod]. |
| // |
| // Excluding operations you're not interested in can save quite a bit of CPU |
| // time; in some use cases there may be hundreds of thousands of useless Write |
| // or Chmod operations per second. |
| // |
| // This can also be used to add unportable operations not supported by all |
| // platforms; unportable operations all start with "Unportable": |
| // [UnportableOpen], [UnportableRead], [UnportableCloseWrite], and |
| // [UnportableCloseRead]. |
| // |
| // AddWith returns an error when using an unportable operation that's not |
| // supported. Use [Watcher.Support] to check for support. |
| func withOps(op Op) addOpt { |
| return func(opt *withOpts) { opt.op = op } |
| } |
| |
| // WithNoFollow disables following symlinks, so the symlinks themselves are |
| // watched. |
| func withNoFollow() addOpt { |
| return func(opt *withOpts) { opt.noFollow = true } |
| } |
| |
| // "Internal" option for recursive watches on inotify. |
| func withCreate() addOpt { |
| return func(opt *withOpts) { opt.sendCreate = true } |
| } |
| |
| var enableRecurse = false |
| |
| // Check if this path is recursive (ends with "/..." or "\..."), and return the |
| // path with the /... stripped. |
| func recursivePath(path string) (string, bool) { |
| path = filepath.Clean(path) |
| if !enableRecurse { // Only enabled in tests for now. |
| return path, false |
| } |
| if filepath.Base(path) == "..." { |
| return filepath.Dir(path), true |
| } |
| return path, false |
| } |