| //go:build solaris |
| // +build solaris |
| |
| // Note: the documentation on the Watcher type and methods is generated from |
| // mkdoc.zsh |
| |
| package fsnotify |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| "sync" |
| |
| "golang.org/x/sys/unix" |
| ) |
| |
| // 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 times, 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 { |
| // 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. |
| // |
| // ErrEventOverflow is used to indicate there are too many events: |
| // |
| // - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl) |
| // - windows: The buffer size is too small; WithBufferSize() can be used to increase it. |
| // - kqueue, fen: Not used. |
| Errors chan error |
| |
| mu sync.Mutex |
| port *unix.EventPort |
| done chan struct{} // Channel for sending a "quit message" to the reader goroutine |
| dirs map[string]struct{} // Explicitly watched directories |
| watches map[string]struct{} // Explicitly watched non-directories |
| } |
| |
| // NewWatcher creates a new Watcher. |
| func NewWatcher() (*Watcher, error) { |
| return NewBufferedWatcher(0) |
| } |
| |
| // 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) { |
| w := &Watcher{ |
| Events: make(chan Event, sz), |
| Errors: make(chan error), |
| dirs: make(map[string]struct{}), |
| watches: make(map[string]struct{}), |
| done: make(chan struct{}), |
| } |
| |
| var err error |
| w.port, err = unix.NewEventPort() |
| if err != nil { |
| return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err) |
| } |
| |
| go w.readEvents() |
| return w, nil |
| } |
| |
| // sendEvent attempts to send an event to the user, returning true if the event |
| // was put in the channel successfully and false if the watcher has been closed. |
| func (w *Watcher) sendEvent(name string, op Op) (sent bool) { |
| select { |
| case w.Events <- Event{Name: name, Op: op}: |
| return true |
| case <-w.done: |
| return false |
| } |
| } |
| |
| // sendError attempts to send an error to the user, returning true if the error |
| // was put in the channel successfully and false if the watcher has been closed. |
| func (w *Watcher) sendError(err error) (sent bool) { |
| select { |
| case w.Errors <- err: |
| return true |
| case <-w.done: |
| return false |
| } |
| } |
| |
| func (w *Watcher) isClosed() bool { |
| select { |
| case <-w.done: |
| return true |
| default: |
| return false |
| } |
| } |
| |
| // Close removes all watches and closes the Events channel. |
| func (w *Watcher) Close() error { |
| // Take the lock used by associateFile to prevent lingering events from |
| // being processed after the close |
| w.mu.Lock() |
| defer w.mu.Unlock() |
| if w.isClosed() { |
| return nil |
| } |
| close(w.done) |
| return w.port.Close() |
| } |
| |
| // 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(name string) error { return w.AddWith(name) } |
| |
| // 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(name string, opts ...addOpt) error { |
| if w.isClosed() { |
| return ErrClosed |
| } |
| if w.port.PathIsWatched(name) { |
| return nil |
| } |
| |
| _ = getOptions(opts...) |
| |
| // Currently we resolve symlinks that were explicitly requested to be |
| // watched. Otherwise we would use LStat here. |
| stat, err := os.Stat(name) |
| if err != nil { |
| return err |
| } |
| |
| // Associate all files in the directory. |
| if stat.IsDir() { |
| err := w.handleDirectory(name, stat, true, w.associateFile) |
| if err != nil { |
| return err |
| } |
| |
| w.mu.Lock() |
| w.dirs[name] = struct{}{} |
| w.mu.Unlock() |
| return nil |
| } |
| |
| err = w.associateFile(name, stat, true) |
| if err != nil { |
| return err |
| } |
| |
| w.mu.Lock() |
| w.watches[name] = struct{}{} |
| w.mu.Unlock() |
| return nil |
| } |
| |
| // 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(name string) error { |
| if w.isClosed() { |
| return nil |
| } |
| if !w.port.PathIsWatched(name) { |
| return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) |
| } |
| |
| // The user has expressed an intent. Immediately remove this name from |
| // whichever watch list it might be in. If it's not in there the delete |
| // doesn't cause harm. |
| w.mu.Lock() |
| delete(w.watches, name) |
| delete(w.dirs, name) |
| w.mu.Unlock() |
| |
| stat, err := os.Stat(name) |
| if err != nil { |
| return err |
| } |
| |
| // Remove associations for every file in the directory. |
| if stat.IsDir() { |
| err := w.handleDirectory(name, stat, false, w.dissociateFile) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| err = w.port.DissociatePath(name) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // readEvents contains the main loop that runs in a goroutine watching for events. |
| func (w *Watcher) readEvents() { |
| // If this function returns, the watcher has been closed and we can close |
| // these channels |
| defer func() { |
| close(w.Errors) |
| close(w.Events) |
| }() |
| |
| pevents := make([]unix.PortEvent, 8) |
| for { |
| count, err := w.port.Get(pevents, 1, nil) |
| if err != nil && err != unix.ETIME { |
| // Interrupted system call (count should be 0) ignore and continue |
| if errors.Is(err, unix.EINTR) && count == 0 { |
| continue |
| } |
| // Get failed because we called w.Close() |
| if errors.Is(err, unix.EBADF) && w.isClosed() { |
| return |
| } |
| // There was an error not caused by calling w.Close() |
| if !w.sendError(err) { |
| return |
| } |
| } |
| |
| p := pevents[:count] |
| for _, pevent := range p { |
| if pevent.Source != unix.PORT_SOURCE_FILE { |
| // Event from unexpected source received; should never happen. |
| if !w.sendError(errors.New("Event from unexpected source received")) { |
| return |
| } |
| continue |
| } |
| |
| err = w.handleEvent(&pevent) |
| if err != nil { |
| if !w.sendError(err) { |
| return |
| } |
| } |
| } |
| } |
| } |
| |
| func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error { |
| files, err := os.ReadDir(path) |
| if err != nil { |
| return err |
| } |
| |
| // Handle all children of the directory. |
| for _, entry := range files { |
| finfo, err := entry.Info() |
| if err != nil { |
| return err |
| } |
| err = handler(filepath.Join(path, finfo.Name()), finfo, false) |
| if err != nil { |
| return err |
| } |
| } |
| |
| // And finally handle the directory itself. |
| return handler(path, stat, follow) |
| } |
| |
| // handleEvent might need to emit more than one fsnotify event if the events |
| // bitmap matches more than one event type (e.g. the file was both modified and |
| // had the attributes changed between when the association was created and the |
| // when event was returned) |
| func (w *Watcher) handleEvent(event *unix.PortEvent) error { |
| var ( |
| events = event.Events |
| path = event.Path |
| fmode = event.Cookie.(os.FileMode) |
| reRegister = true |
| ) |
| |
| w.mu.Lock() |
| _, watchedDir := w.dirs[path] |
| _, watchedPath := w.watches[path] |
| w.mu.Unlock() |
| isWatched := watchedDir || watchedPath |
| |
| if events&unix.FILE_DELETE != 0 { |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| reRegister = false |
| } |
| if events&unix.FILE_RENAME_FROM != 0 { |
| if !w.sendEvent(path, Rename) { |
| return nil |
| } |
| // Don't keep watching the new file name |
| reRegister = false |
| } |
| if events&unix.FILE_RENAME_TO != 0 { |
| // We don't report a Rename event for this case, because Rename events |
| // are interpreted as referring to the _old_ name of the file, and in |
| // this case the event would refer to the new name of the file. This |
| // type of rename event is not supported by fsnotify. |
| |
| // inotify reports a Remove event in this case, so we simulate this |
| // here. |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| // Don't keep watching the file that was removed |
| reRegister = false |
| } |
| |
| // The file is gone, nothing left to do. |
| if !reRegister { |
| if watchedDir { |
| w.mu.Lock() |
| delete(w.dirs, path) |
| w.mu.Unlock() |
| } |
| if watchedPath { |
| w.mu.Lock() |
| delete(w.watches, path) |
| w.mu.Unlock() |
| } |
| return nil |
| } |
| |
| // If we didn't get a deletion the file still exists and we're going to have |
| // to watch it again. Let's Stat it now so that we can compare permissions |
| // and have what we need to continue watching the file |
| |
| stat, err := os.Lstat(path) |
| if err != nil { |
| // This is unexpected, but we should still emit an event. This happens |
| // most often on "rm -r" of a subdirectory inside a watched directory We |
| // get a modify event of something happening inside, but by the time we |
| // get here, the sudirectory is already gone. Clearly we were watching |
| // this path but now it is gone. Let's tell the user that it was |
| // removed. |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| // Suppress extra write events on removed directories; they are not |
| // informative and can be confusing. |
| return nil |
| } |
| |
| // resolve symlinks that were explicitly watched as we would have at Add() |
| // time. this helps suppress spurious Chmod events on watched symlinks |
| if isWatched { |
| stat, err = os.Stat(path) |
| if err != nil { |
| // The symlink still exists, but the target is gone. Report the |
| // Remove similar to above. |
| if !w.sendEvent(path, Remove) { |
| return nil |
| } |
| // Don't return the error |
| } |
| } |
| |
| if events&unix.FILE_MODIFIED != 0 { |
| if fmode.IsDir() { |
| if watchedDir { |
| if err := w.updateDirectory(path); err != nil { |
| return err |
| } |
| } else { |
| if !w.sendEvent(path, Write) { |
| return nil |
| } |
| } |
| } else { |
| if !w.sendEvent(path, Write) { |
| return nil |
| } |
| } |
| } |
| if events&unix.FILE_ATTRIB != 0 && stat != nil { |
| // Only send Chmod if perms changed |
| if stat.Mode().Perm() != fmode.Perm() { |
| if !w.sendEvent(path, Chmod) { |
| return nil |
| } |
| } |
| } |
| |
| if stat != nil { |
| // If we get here, it means we've hit an event above that requires us to |
| // continue watching the file or directory |
| return w.associateFile(path, stat, isWatched) |
| } |
| return nil |
| } |
| |
| func (w *Watcher) updateDirectory(path string) error { |
| // The directory was modified, so we must find unwatched entities and watch |
| // them. If something was removed from the directory, nothing will happen, |
| // as everything else should still be watched. |
| files, err := os.ReadDir(path) |
| if err != nil { |
| return err |
| } |
| |
| for _, entry := range files { |
| path := filepath.Join(path, entry.Name()) |
| if w.port.PathIsWatched(path) { |
| continue |
| } |
| |
| finfo, err := entry.Info() |
| if err != nil { |
| return err |
| } |
| err = w.associateFile(path, finfo, false) |
| if err != nil { |
| if !w.sendError(err) { |
| return nil |
| } |
| } |
| if !w.sendEvent(path, Create) { |
| return nil |
| } |
| } |
| return nil |
| } |
| |
| func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error { |
| if w.isClosed() { |
| return ErrClosed |
| } |
| // This is primarily protecting the call to AssociatePath but it is |
| // important and intentional that the call to PathIsWatched is also |
| // protected by this mutex. Without this mutex, AssociatePath has been seen |
| // to error out that the path is already associated. |
| w.mu.Lock() |
| defer w.mu.Unlock() |
| |
| if w.port.PathIsWatched(path) { |
| // Remove the old association in favor of this one If we get ENOENT, |
| // then while the x/sys/unix wrapper still thought that this path was |
| // associated, the underlying event port did not. This call will have |
| // cleared up that discrepancy. The most likely cause is that the event |
| // has fired but we haven't processed it yet. |
| err := w.port.DissociatePath(path) |
| if err != nil && err != unix.ENOENT { |
| return err |
| } |
| } |
| // FILE_NOFOLLOW means we watch symlinks themselves rather than their |
| // targets. |
| events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW |
| if follow { |
| // We *DO* follow symlinks for explicitly watched entries. |
| events = unix.FILE_MODIFIED | unix.FILE_ATTRIB |
| } |
| return w.port.AssociatePath(path, stat, |
| events, |
| stat.Mode()) |
| } |
| |
| func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error { |
| if !w.port.PathIsWatched(path) { |
| return nil |
| } |
| return w.port.DissociatePath(path) |
| } |
| |
| // 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 { |
| if w.isClosed() { |
| return nil |
| } |
| |
| w.mu.Lock() |
| defer w.mu.Unlock() |
| |
| entries := make([]string, 0, len(w.watches)+len(w.dirs)) |
| for pathname := range w.dirs { |
| entries = append(entries, pathname) |
| } |
| for pathname := range w.watches { |
| entries = append(entries, pathname) |
| } |
| |
| return entries |
| } |