123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- //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
- }
|