//go:build linux && !appengine package fsnotify import ( "errors" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "sync" "time" "unsafe" "github.com/fsnotify/fsnotify/internal" "golang.org/x/sys/unix" ) type inotify struct { *shared Events chan Event 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 inotifyFile *os.File watches *watches doneResp chan struct{} // Channel to respond to Close // Store rename cookies in an array, with the index wrapping to 0. Almost // all of the time what we get is a MOVED_FROM to set the cookie and the // next event inotify sends will be MOVED_TO to read it. However, this is // not guaranteed – as described in inotify(7) – and we may get other events // between the two MOVED_* events (including other MOVED_* ones). // // A second issue is that moving a file outside the watched directory will // trigger a MOVED_FROM to set the cookie, but we never see the MOVED_TO to // read and delete it. So just storing it in a map would slowly leak memory. // // Doing it like this gives us a simple fast LRU-cache that won't allocate. // Ten items should be more than enough for our purpose, and a loop over // such a short array is faster than a map access anyway (not that it hugely // matters since we're talking about hundreds of ns at the most, but still). cookies [10]koekje cookieIndex uint8 cookiesMu sync.Mutex } type ( watches struct { wd map[uint32]*watch // wd → watch path map[string]uint32 // pathname → wd } watch struct { wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) path string // Watch path. watchFlags watchFlag } koekje struct { cookie uint32 path string } ) func (w watch) byUser() bool { return w.watchFlags&flagByUser != 0 } func (w watch) recurse() bool { return w.watchFlags&flagRecurse != 0 } func newWatches() *watches { return &watches{ wd: make(map[uint32]*watch), path: make(map[string]uint32), } } func (w *watches) byPath(path string) *watch { return w.wd[w.path[path]] } func (w *watches) byWd(wd uint32) *watch { return w.wd[wd] } func (w *watches) len() int { return len(w.wd) } func (w *watches) add(ww *watch) { w.wd[ww.wd] = ww; w.path[ww.path] = ww.wd } func (w *watches) remove(watch *watch) { delete(w.path, watch.path); delete(w.wd, watch.wd) } func (w *watches) removePath(path string) ([]uint32, error) { path, recurse := recursivePath(path) wd, ok := w.path[path] if !ok { return nil, fmt.Errorf("%w: %s", ErrNonExistentWatch, path) } watch := w.wd[wd] if recurse && !watch.recurse() { return nil, fmt.Errorf("can't use /... with non-recursive watch %q", path) } delete(w.path, path) delete(w.wd, wd) if !watch.recurse() { return []uint32{wd}, nil } wds := make([]uint32, 0, 8) wds = append(wds, wd) for p, rwd := range w.path { if strings.HasPrefix(p, path) { delete(w.path, p) delete(w.wd, rwd) wds = append(wds, rwd) } } return wds, nil } func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error { var existing *watch wd, ok := w.path[path] if ok { existing = w.wd[wd] } upd, err := f(existing) if err != nil { return err } if upd != nil { w.wd[upd.wd] = upd w.path[upd.path] = upd.wd if upd.wd != wd { delete(w.wd, wd) } } return nil } var defaultBufferSize = 0 func newBackend(ev chan Event, errs chan error) (backend, error) { // Need to set nonblocking mode for SetDeadline to work, otherwise blocking // I/O operations won't terminate on close. fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) if fd == -1 { return nil, errno } w := &inotify{ shared: newShared(ev, errs), Events: ev, Errors: errs, fd: fd, inotifyFile: os.NewFile(uintptr(fd), ""), watches: newWatches(), doneResp: make(chan struct{}), } go w.readEvents() return w, nil } func (w *inotify) Close() error { if w.shared.close() { return nil } // Causes any blocking reads to return with an error, provided the file // still supports deadline operations. err := w.inotifyFile.Close() if err != nil { return err } <-w.doneResp // Wait for readEvents() to finish. return nil } func (w *inotify) Add(name string) error { return w.AddWith(name) } func (w *inotify) AddWith(path string, opts ...addOpt) error { if w.isClosed() { return ErrClosed } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s AddWith(%q)\n", time.Now().Format("15:04:05.000000000"), path) } with := getOptions(opts...) if !w.xSupports(with.op) { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } add := func(path string, with withOpts, wf watchFlag) error { var flags uint32 if with.op.Has(Create) { flags |= unix.IN_CREATE } if with.op.Has(Write) { flags |= unix.IN_MODIFY } if with.op.Has(Remove) { flags |= unix.IN_DELETE | unix.IN_DELETE_SELF } if with.op.Has(Rename) { flags |= unix.IN_MOVED_TO | unix.IN_MOVED_FROM | unix.IN_MOVE_SELF } if with.op.Has(Chmod) { flags |= unix.IN_ATTRIB } if with.op.Has(xUnportableOpen) { flags |= unix.IN_OPEN } if with.op.Has(xUnportableRead) { flags |= unix.IN_ACCESS } if with.op.Has(xUnportableCloseWrite) { flags |= unix.IN_CLOSE_WRITE } if with.op.Has(xUnportableCloseRead) { flags |= unix.IN_CLOSE_NOWRITE } return w.register(path, flags, wf) } w.mu.Lock() defer w.mu.Unlock() path, recurse := recursivePath(path) if recurse { return filepath.WalkDir(path, func(root string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { if root == path { return fmt.Errorf("fsnotify: not a directory: %q", path) } return nil } // Send a Create event when adding new directory from a recursive // watch; this is for "mkdir -p one/two/three". Usually all those // directories will be created before we can set up watchers on the // subdirectories, so only "one" would be sent as a Create event and // not "one/two" and "one/two/three" (inotifywait -r has the same // problem). if with.sendCreate && root != path { w.sendEvent(Event{Name: root, Op: Create}) } wf := flagRecurse if root == path { wf |= flagByUser } return add(root, with, wf) }) } return add(path, with, 0) } func (w *inotify) register(path string, flags uint32, wf watchFlag) error { return w.watches.updatePath(path, func(existing *watch) (*watch, error) { if existing != nil { flags |= existing.flags | unix.IN_MASK_ADD } wd, err := unix.InotifyAddWatch(w.fd, path, flags) if wd == -1 { return nil, err } if e, ok := w.watches.wd[uint32(wd)]; ok { return e, nil } if existing == nil { return &watch{ wd: uint32(wd), path: path, flags: flags, watchFlags: wf, }, nil } existing.wd = uint32(wd) existing.flags = flags return existing, nil }) } func (w *inotify) Remove(name string) error { if w.isClosed() { return nil } if debug { fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s Remove(%q)\n", time.Now().Format("15:04:05.000000000"), name) } w.mu.Lock() defer w.mu.Unlock() return w.remove(filepath.Clean(name)) } func (w *inotify) remove(name string) error { wds, err := w.watches.removePath(name) if err != nil { return err } for _, wd := range wds { _, err := unix.InotifyRmWatch(w.fd, wd) if err != nil { // TODO: Perhaps it's not helpful to return an error here in every // case; the only two possible errors are: // // EBADF, which happens when w.fd is not a valid file descriptor of // any kind. // // EINVAL, which is when fd is not an inotify descriptor or wd is // not a valid watch descriptor. Watch descriptors are invalidated // when they are removed explicitly or implicitly; explicitly by // inotify_rm_watch, implicitly when the file they are watching is // deleted. return err } } return nil } func (w *inotify) WatchList() []string { if w.isClosed() { return nil } w.mu.Lock() defer w.mu.Unlock() entries := make([]string, 0, w.watches.len()) for pathname := range w.watches.path { entries = append(entries, pathname) } return entries } // readEvents reads from the inotify file descriptor, converts the // received events into Event objects and sends them via the Events channel func (w *inotify) readEvents() { defer func() { close(w.doneResp) close(w.Errors) close(w.Events) }() var buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events for { if w.isClosed() { return } n, err := w.inotifyFile.Read(buf[:]) if err != nil { if errors.Is(err, os.ErrClosed) { return } if !w.sendError(err) { return } continue } if n < unix.SizeofInotifyEvent { err := errors.New("notify: short read in readEvents()") // Read was too short. if n == 0 { err = io.EOF // If EOF is received. This should really never happen. } if !w.sendError(err) { return } continue } // We don't know how many events we just read into the buffer While the // offset points to at least one whole event. var offset uint32 for offset <= uint32(n-unix.SizeofInotifyEvent) { // Point to the event in the buffer. inEvent := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) if inEvent.Mask&unix.IN_Q_OVERFLOW != 0 { if !w.sendError(ErrEventOverflow) { return } } ev, ok := w.handleEvent(inEvent, &buf, offset) if !ok { return } if !w.sendEvent(ev) { return } // Move to the next event in the buffer offset += unix.SizeofInotifyEvent + inEvent.Len } } } func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offset uint32) (Event, bool) { w.mu.Lock() defer w.mu.Unlock() /// If the event happened to the watched directory or the watched file, the /// kernel doesn't append the filename to the event, but we would like to /// always fill the the "Name" field with a valid filename. We retrieve the /// path of the watch from the "paths" map. /// /// Can be nil if Remove() was called in another goroutine for this path /// inbetween reading the events from the kernel and reading the internal /// state. Not much we can do about it, so just skip. See #616. watch := w.watches.byWd(uint32(inEvent.Wd)) if watch == nil { return Event{}, true } var ( name = watch.path nameLen = uint32(inEvent.Len) ) if nameLen > 0 { /// Point "bytes" at the first byte of the filename bb := *buf bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] /// The filename is padded with NULL bytes. TrimRight() gets rid of those. name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00") } if debug { internal.Debug(name, inEvent.Mask, inEvent.Cookie) } if inEvent.Mask&unix.IN_IGNORED != 0 || inEvent.Mask&unix.IN_UNMOUNT != 0 { w.watches.remove(watch) return Event{}, true } // inotify will automatically remove the watch on deletes; just need // to clean our state here. if inEvent.Mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { w.watches.remove(watch) } // We can't really update the state when a watched path is moved; only // IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove the watch. if inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { // Watch is set up as part of recurse: do nothing as the move gets // registered from the parent directory. if watch.recurse() && !watch.byUser() { return Event{}, true } err := w.remove(watch.path) if err != nil && !errors.Is(err, ErrNonExistentWatch) { if !w.sendError(err) { return Event{}, false } } if watch.recurse() { return Event{Name: watch.path, Op: Rename}, true } } /// Skip if we're watching both this path and the parent; the parent will /// already send a delete so no need to do it twice. if inEvent.Mask&unix.IN_DELETE_SELF != 0 { _, ok := w.watches.path[filepath.Dir(watch.path)] if ok { return Event{}, true } } ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie) // Need to update watch path for recurse. if watch.recurse() { isDir := inEvent.Mask&unix.IN_ISDIR == unix.IN_ISDIR /// New directory created: set up watch on it. if isDir && ev.Has(Create) { err := w.register(ev.Name, watch.flags, flagRecurse) if !w.sendError(err) { return Event{}, false } // This was a directory rename, so we need to update all the // children. // // TODO: this is of course pretty slow; we should use a better data // structure for storing all of this, e.g. store children in the // watch. I have some code for this in my kqueue refactor we can use // in the future. For now I'm okay with this as it's not publicly // available. Correctness first, performance second. if ev.renamedFrom != "" { for k, ww := range w.watches.wd { if k == watch.wd || ww.path == ev.Name { continue } if strings.HasPrefix(ww.path, ev.renamedFrom) { ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1) w.watches.wd[k] = ww } } } } } return ev, true } func (w *inotify) newEvent(name string, mask, cookie uint32) Event { e := Event{Name: name} if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { e.Op |= Create } if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE { e.Op |= Remove } if mask&unix.IN_MODIFY == unix.IN_MODIFY { e.Op |= Write } if mask&unix.IN_OPEN == unix.IN_OPEN { e.Op |= xUnportableOpen } if mask&unix.IN_ACCESS == unix.IN_ACCESS { e.Op |= xUnportableRead } if mask&unix.IN_CLOSE_WRITE == unix.IN_CLOSE_WRITE { e.Op |= xUnportableCloseWrite } if mask&unix.IN_CLOSE_NOWRITE == unix.IN_CLOSE_NOWRITE { e.Op |= xUnportableCloseRead } if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { e.Op |= Rename } if mask&unix.IN_ATTRIB == unix.IN_ATTRIB { e.Op |= Chmod } if cookie != 0 { if mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM { w.cookiesMu.Lock() w.cookies[w.cookieIndex] = koekje{cookie: cookie, path: e.Name} w.cookieIndex++ if w.cookieIndex > 9 { w.cookieIndex = 0 } w.cookiesMu.Unlock() } else if mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO { w.cookiesMu.Lock() var prev string for _, c := range w.cookies { if c.cookie == cookie { prev = c.path break } } w.cookiesMu.Unlock() e.renamedFrom = prev } } return e } func (w *inotify) xSupports(op Op) bool { return true // Supports everything. } func (w *inotify) state() { w.mu.Lock() defer w.mu.Unlock() for wd, ww := range w.watches.wd { fmt.Fprintf(os.Stderr, "%4d: %q watchFlags=0x%x\n", wd, ww.path, ww.watchFlags) } }
// 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. 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. //lint:ignore ST1012 not relevant xErrUnsupported = errors.New("fsnotify: not supported with this backend") ) // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { ev, errs := make(chan Event, defaultBufferSize), 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, sz), make(chan error) b, err := newBackend(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 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). // // The order is undefined, and may differ per call. 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 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 } } // "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 } type watchFlag uint8 const ( // Added by user with Add(), rather than an internal watch. flagByUser = watchFlag(0x01) // Part of recursive watch; as the top-level path added by the user or an // "internal" watch. flagRecurse = watchFlag(0x02) )
package fsnotify import "sync" type shared struct { Events chan Event Errors chan error done chan struct{} mu sync.Mutex } func newShared(ev chan Event, errs chan error) *shared { return &shared{ Events: ev, Errors: errs, done: make(chan struct{}), } } // Returns true if the event was sent, or false if watcher is closed. func (w *shared) sendEvent(e Event) bool { if e.Op == 0 { return true } select { case <-w.done: return false case w.Events <- e: return true } } // Returns true if the error was sent, or false if watcher is closed. func (w *shared) sendError(err error) bool { if err == nil { return true } select { case <-w.done: return false case w.Errors <- err: return true } } func (w *shared) isClosed() bool { select { case <-w.done: return true default: return false } } // Mark as closed; returns true if it was already closed. func (w *shared) close() bool { w.mu.Lock() defer w.mu.Unlock() if w.isClosed() { return true } close(w.done) return false }