//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 { 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 done chan struct{} // Channel for sending a "quit message" to the reader goroutine doneMu sync.Mutex 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 { mu sync.RWMutex 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. recurse bool // Recursion with ./...? } koekje struct { cookie uint32 path string } ) func newWatches() *watches { return &watches{ wd: make(map[uint32]*watch), path: make(map[string]uint32), } } func (w *watches) len() int { w.mu.RLock() defer w.mu.RUnlock() return len(w.wd) } func (w *watches) add(ww *watch) { w.mu.Lock() defer w.mu.Unlock() w.wd[ww.wd] = ww w.path[ww.path] = ww.wd } func (w *watches) remove(wd uint32) { w.mu.Lock() defer w.mu.Unlock() delete(w.path, w.wd[wd].path) delete(w.wd, wd) } func (w *watches) removePath(path string) ([]uint32, error) { w.mu.Lock() defer w.mu.Unlock() 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 filepath.HasPrefix(p, path) { delete(w.path, p) delete(w.wd, rwd) wds = append(wds, rwd) } } return wds, nil } func (w *watches) byPath(path string) *watch { w.mu.RLock() defer w.mu.RUnlock() return w.wd[w.path[path]] } func (w *watches) byWd(wd uint32) *watch { w.mu.RLock() defer w.mu.RUnlock() return w.wd[wd] } func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error { w.mu.Lock() defer w.mu.Unlock() 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 } func newBackend(ev chan Event, errs chan error) (backend, error) { return newBufferedBackend(0, ev, errs) } func newBufferedBackend(sz uint, 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{ Events: ev, Errors: errs, fd: fd, inotifyFile: os.NewFile(uintptr(fd), ""), watches: newWatches(), done: make(chan struct{}), doneResp: make(chan struct{}), } go w.readEvents() return w, nil } // Returns true if the event was sent, or false if watcher is closed. func (w *inotify) sendEvent(e Event) bool { 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 *inotify) sendError(err error) bool { if err == nil { return true } select { case <-w.done: return false case w.Errors <- err: return true } } func (w *inotify) isClosed() bool { select { case <-w.done: return true default: return false } } func (w *inotify) Close() error { w.doneMu.Lock() if w.isClosed() { w.doneMu.Unlock() return nil } close(w.done) w.doneMu.Unlock() // 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 } // Wait for goroutine to close <-w.doneResp 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) } 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}) } return w.add(root, with, true) }) } return w.add(path, with, false) } func (w *inotify) add(path string, with withOpts, recurse bool) error { var flags uint32 if with.noFollow { flags |= unix.IN_DONT_FOLLOW } 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, recurse) } func (w *inotify) register(path string, flags uint32, recurse bool) 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 existing == nil { return &watch{ wd: uint32(wd), path: path, flags: flags, recurse: recurse, }, 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) } 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 } entries := make([]string, 0, w.watches.len()) w.watches.mu.RLock() for pathname := range w.watches.path { entries = append(entries, pathname) } w.watches.mu.RUnlock() 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 errno error // Syscall errno ) for { // See if we have been closed. if w.isClosed() { return } n, err := w.inotifyFile.Read(buf[:]) switch { case errors.Unwrap(err) == os.ErrClosed: return case err != nil: if !w.sendError(err) { return } continue } if n < unix.SizeofInotifyEvent { var err error if n == 0 { err = io.EOF // If EOF is received. This should really never happen. } else if n < 0 { err = errno // If an error occurred while reading. } else { err = errors.New("notify: short read in readEvents()") // Read was too short. } 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) { var ( // Point "raw" to the event in the buffer raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset])) mask = uint32(raw.Mask) nameLen = uint32(raw.Len) // Move to the next event in the buffer next = func() { offset += unix.SizeofInotifyEvent + nameLen } ) if mask&unix.IN_Q_OVERFLOW != 0 { if !w.sendError(ErrEventOverflow) { return } } // 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. watch := w.watches.byWd(uint32(raw.Wd)) var name string if watch != nil { name = watch.path } if nameLen > 0 { // Point "bytes" at the first byte of the filename bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] // The filename is padded with NULL bytes. TrimRight() gets rid of those. name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") } if debug { internal.Debug(name, raw.Mask, raw.Cookie) } if mask&unix.IN_IGNORED != 0 { //&& event.Op != 0 next() continue } // inotify will automatically remove the watch on deletes; just need // to clean our state here. if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { w.watches.remove(watch.wd) } // 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 watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { if watch.recurse { next() // Do nothing continue } err := w.remove(watch.path) if err != nil && !errors.Is(err, ErrNonExistentWatch) { if !w.sendError(err) { return } } } /// 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 mask&unix.IN_DELETE_SELF != 0 { if _, ok := w.watches.path[filepath.Dir(watch.path)]; ok { next() continue } } ev := w.newEvent(name, mask, raw.Cookie) // Need to update watch path for recurse. if watch != nil && watch.recurse { isDir := 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, true) if !w.sendError(err) { return } // 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 != "" { w.watches.mu.Lock() 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 } } w.watches.mu.Unlock() } } } /// Send the events that are not ignored on the events channel if !w.sendEvent(ev) { return } next() } } } func (w *inotify) isRecursive(path string) bool { ww := w.watches.byPath(path) if ww == nil { // path could be a file, so also check the Dir. ww = w.watches.byPath(filepath.Dir(path)) } return ww != nil && ww.recurse } 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.watches.mu.Lock() defer w.watches.mu.Unlock() for wd, ww := range w.watches.wd { fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path) } }