//go:build linux package fswatch import ( "encoding/binary" "errors" "fmt" "sync/atomic" "unsafe" "golang.org/x/sys/unix" ) // --------------------------------------------------------------------------- // fanotify_linux.go: Linux fanotify backend // // Uses Linux's fanotify(7) API (kernel ≥ 5.13 without CAP_SYS_ADMIN) to // watch directory trees. Unlike inotify, fanotify uses FID-based event // reporting (FAN_REPORT_FID | FAN_REPORT_DFID_NAME): each event carries the // parent directory's file handle and the child entry name, so watch // dispatch is keyed by (fsid, handle_type, handle_bytes) instead of a wd // integer. This avoids the inotify per-user watch limit (fs.inotify. // max_user_watches) entirely. // // ┌──────────────────────────────────────────────────────────────┐ // │ fanotifyBackend │ // │ │ // │ ┌───────────┐ poll(2) ┌──────────────────┐ │ // │ │ pipe[0] ├──────────────────────►│ │ │ // │ │ (wakeup) │ │ start() │ │ // │ └───────────┘ │ goroutine │ │ // │ ┌───────────┐ │ (event loop) │ │ // │ │ fanotify ├──────────────────────►│ │ │ // │ │ fd │ └────────┬─────────┘ │ // │ └───────────┘ │ │ // │ handleEvents() │ // │ │ │ // │ parseFanotifyDfidNames │ // │ (extract handleKey + name) │ // │ │ │ // │ ▼ │ // │ ┌─────────────────────────┐ │ // │ │ subscriptions │ │ // │ │ map[handleKey] → []sub │ │ // │ │ sub.dirWatch.events │ │ // │ └─────────────────────────┘ │ // │ │ // │ handleKey = (fsid, handle_type, handle_bytes) │ // │ obtained via statfs(2) + name_to_handle_at(2) per dir │ // └──────────────────────────────────────────────────────────────┘ // // Goroutines and threading: // - One long-lived goroutine (start), launched by watcherBase.run(). It // owns the poll(2) loop and runs for the process lifetime. All event // reading and dispatch (handleEvents, handleParsedEvent, // handleSubscription, handleRenameEvent) execute on this goroutine, // under b.mu. // - subscribe/closeWatch run on the caller's goroutine under // watcherBase.mu. The event loop acquires b.mu for watch map // access, providing safe interleaving. // // Callback delivery: // dirWatch.notify() posts to the shared process-wide debouncer. After a // coalescing window (50 ms min / 500 ms max), the debouncer invokes all // registered WatchCallbacks on its own dedicated goroutine; never on // the caller's goroutine or the event-loop goroutine. // // WatchDirectory flow (caller goroutine): // 1. Walk the target directory. // 2. On the first subscribe, probe FAN_RENAME support (Linux 5.17+) by // attempting a fanotify_mark with FAN_RENAME. If the kernel returns // EINVAL or EOPNOTSUPP, fall back to FAN_MOVED_FROM | FAN_MOVED_TO // (two separate events instead of one paired event for renames). // 3. For every directory found: // a. fanotify_mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR) to watch it. // b. name_to_handle_at(2) to obtain the directory's file handle. // c. statfs(2) to obtain the filesystem ID (fsid). // d. Map (fsid, handle_type, handle_bytes) → fanotifySubscription. // // Event format: // Each event has a FanotifyEventMetadata header followed by variable-length // info records. parseFanotifyDfidNames extracts DFID_NAME records // (FAN_EVENT_INFO_TYPE_DFID_NAME, OLD_DFID_NAME, NEW_DFID_NAME) containing // the parent directory's file handle and child entry name. The file handle // is matched against the watch map to find the watched directory. // // Event dispatch (on start goroutine): // - FAN_CREATE / FAN_MOVED_TO → events.create (→ EventUpdate); if the new // entry is a directory (FAN_ONDIR), recursively walk and mark it. // - FAN_MODIFY → events.update (→ EventUpdate). // - FAN_DELETE* / FAN_MOVE* → events.remove (→ EventDelete); drop // subscriptions for the removed path and any descendants. // - FAN_RENAME (5.17+) → single paired event with OLD_DFID_NAME + // NEW_DFID_NAME info records; handleRenameEvent deletes the old path and // creates the new path in one pass. // - FAN_Q_OVERFLOW → set ErrOverflow on every active dirWatch. // // Merged events: fanotify can merge consecutive events on the same object // into one event with multiple mask bits. When both create and delete bits // are set, handleSubscription stats the path to determine which happened // last (exists → delete-then-create = update; gone → create-then-delete = // events cancel out). // // After processing all buffered events, call dirWatch.notify() on each // touched dirWatch to trigger the debouncer. // // Shutdown: // Write a byte to pipe[1] → poll sees POLLIN on pipe[0] → loop exits → // deferred closeFDs closes fanotify fd, pipe fds, and signals endedSignal. // --------------------------------------------------------------------------- const ( fanotifyInitFlags uint = unix.FAN_CLASS_NOTIF | unix.FAN_CLOEXEC | unix.FAN_NONBLOCK | unix.FAN_REPORT_FID | unix.FAN_REPORT_DFID_NAME fanotifyMarkMaskBase uint64 = unix.FAN_CREATE | unix.FAN_DELETE | unix.FAN_MODIFY | unix.FAN_DELETE_SELF | unix.FAN_MOVE_SELF | unix.FAN_ONDIR | unix.FAN_EVENT_ON_CHILD // Used when FAN_RENAME is available (Linux 5.17+). fanotifyMarkMaskRename uint64 = fanotifyMarkMaskBase | unix.FAN_RENAME // Fallback when FAN_RENAME is not available. fanotifyMarkMaskMovedFromTo uint64 = fanotifyMarkMaskBase | unix.FAN_MOVED_FROM | unix.FAN_MOVED_TO fanotifyMarkAddFlags uint = unix.FAN_MARK_ADD | unix.FAN_MARK_ONLYDIR | unix.FAN_MARK_DONT_FOLLOW fanotifyBufferSize = 8192 ) // fanotifyHandleKey uniquely identifies a filesystem object by its fsid and // file handle. Used as a map key for watch dispatch. type fanotifyHandleKey struct { fsid [2]int32 handleType int32 handle string // raw handle bytes as string for map comparability } func makeFanotifyHandleKey(fsid [2]int32, handleType int32, handleBytes []byte) fanotifyHandleKey { return fanotifyHandleKey{ fsid: fsid, handleType: handleType, handle: string(handleBytes), } } // fanotifySubscription mirrors inotifySubscription for the fanotify backend. type fanotifySubscription struct { path string watchPath string dirWatch *dirWatch key fanotifyHandleKey } // fanotifyDfidName holds parsed directory FID + name from an info record. type fanotifyDfidName struct { key fanotifyHandleKey name string // child entry name, or "" for self-events on directories } // fanotifyBackend is the fanotify-based watcher backend for Linux. type fanotifyBackend struct { watcherBase pipeFDs [2]int pipeWriteFD atomic.Int32 fanotifyFD int markMask uint64 // fanotifyMarkMaskRename or fanotifyMarkMaskMovedFromTo; 0 until first subscribe noRename bool // when true, skip FAN_RENAME probe (for testing fallback path) subscriptions map[fanotifyHandleKey][]*fanotifySubscription endedSignal chan struct{} // Persistent buffers reused across handleEvents calls. Only accessed // from the start goroutine, so no synchronization needed. readBuf []byte watchersTouched map[*dirWatch]struct{} } func init() { if fanotifyAvailable() { fanotifyWatcher.factory = func() watcherImpl { return newFanotifyBackend(false) } } } // fanotifyAvailable probes whether fanotify_init succeeds with the flags // this backend needs. func fanotifyAvailable() bool { fd, err := unix.FanotifyInit(fanotifyInitFlags, unix.O_RDONLY|unix.O_CLOEXEC) if err != nil { return false } _ = unix.Close(fd) return true } // newFanotifyBackend creates a fanotify backend. If noRename is true, the // backend skips the FAN_RENAME probe and forces the FAN_MOVED_FROM/FAN_MOVED_TO // fallback path; this is only used by the fanotify-no-rename test watcher to // exercise the fallback path on kernels that natively support FAN_RENAME. func newFanotifyBackend(noRename bool) *fanotifyBackend { b := &fanotifyBackend{ pipeFDs: [2]int{-1, -1}, fanotifyFD: -1, noRename: noRename, subscriptions: map[fanotifyHandleKey][]*fanotifySubscription{}, endedSignal: make(chan struct{}), readBuf: make([]byte, fanotifyBufferSize), watchersTouched: make(map[*dirWatch]struct{}), } b.pipeWriteFD.Store(-1) b.watcherBase.init(b) return b } func (b *fanotifyBackend) start() error { if err := unix.Pipe2(b.pipeFDs[:], unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil { return fmt.Errorf("unable to open pipe: %w", err) } b.pipeWriteFD.Store(int32(b.pipeFDs[1])) defer func() { b.closeFDs() close(b.endedSignal) }() fd, err := unix.FanotifyInit(fanotifyInitFlags, unix.O_RDONLY|unix.O_CLOEXEC) if err != nil { return fmt.Errorf("unable to initialize fanotify: %w", err) } b.fanotifyFD = fd pollfds := []unix.PollFd{ {Fd: int32(b.pipeFDs[0]), Events: unix.POLLIN}, {Fd: int32(b.fanotifyFD), Events: unix.POLLIN}, } b.notifyStarted() for { _, err := unix.Poll(pollfds, 500) if err != nil { if errors.Is(err, unix.EINTR) { continue } return fmt.Errorf("unable to poll: %w", err) } if pollfds[0].Revents != 0 { break } if pollfds[1].Revents != 0 { if err := b.handleEvents(); err != nil { return err } } } return nil } func (b *fanotifyBackend) closeFDs() { b.mu.Lock() defer b.mu.Unlock() if b.pipeFDs[0] >= 0 { _ = unix.Close(b.pipeFDs[0]) b.pipeFDs[0] = -1 } if fd := b.pipeWriteFD.Swap(-1); fd >= 0 { _ = unix.Close(int(fd)) } b.pipeFDs[1] = -1 if b.fanotifyFD >= 0 { _ = unix.Close(b.fanotifyFD) b.fanotifyFD = -1 } } func (b *fanotifyBackend) shutdown() { fd := b.pipeWriteFD.Load() if fd < 0 { return } _, _ = unix.Write(int(fd), []byte{'X'}) <-b.endedSignal } func (b *fanotifyBackend) subscribe(w *dirWatch) error { // Probe FAN_RENAME on the first subscribe using the actual watch // directory. FAN_RENAME (Linux 5.17+) yields a single paired event // for renames; when unavailable we fall back to FAN_MOVED_FROM/ // FAN_MOVED_TO which produces two separate events but is otherwise // equivalent. The kernel rejects unknown mask bits with EINVAL. if b.markMask == 0 { if b.noRename { b.markMask = fanotifyMarkMaskMovedFromTo } else { b.markMask = fanotifyMarkMaskRename err := unix.FanotifyMark(b.fanotifyFD, fanotifyMarkAddFlags, fanotifyMarkMaskRename, unix.AT_FDCWD, w.physicalDir) switch { case err == nil: // B5: pair the probe Add with a matching Remove. If // Remove fails (rare; only EINTR or kernel resource // pressure realistically) we leave the probe mark // attached for the life of the process, but since // markDir below will Add the real mask with the same // flags the kernel just merges them. The probe is the // only failure path we explicitly retry. for { rmErr := unix.FanotifyMark(b.fanotifyFD, unix.FAN_MARK_REMOVE|unix.FAN_MARK_ONLYDIR, fanotifyMarkMaskRename, unix.AT_FDCWD, w.physicalDir) if rmErr == nil || !errors.Is(rmErr, unix.EINTR) { break } } case errors.Is(err, unix.EINVAL), errors.Is(err, unix.EOPNOTSUPP): b.markMask = fanotifyMarkMaskMovedFromTo } } } if !w.recursive { if err := b.markDir(w, w.dir, w.physicalDir); err != nil { return &dirWatchError{ err: fmt.Errorf("fanotify_mark on '%s' failed: %w", w.dir, err), dirWatch: w, } } return nil } if err := walkDir(w.physicalDir, true, func(watchPath string, isDir bool) error { if !isDir { return nil } path := w.displayPath(watchPath) if err := b.markDir(w, path, watchPath); err != nil { return &dirWatchError{ err: fmt.Errorf("fanotify_mark on '%s' failed: %w", path, err), dirWatch: w, } } return nil }); err != nil { _ = b.closeWatch(w) return err } return nil } func (b *fanotifyBackend) markDir(w *dirWatch, path string, markPath string) error { if err := unix.FanotifyMark(b.fanotifyFD, fanotifyMarkAddFlags, b.markMask, unix.AT_FDCWD, markPath); err != nil { return err } handle, _, err := unix.NameToHandleAt(unix.AT_FDCWD, markPath, 0) if err != nil { // Unmark since we can't track this directory without a handle. _ = unix.FanotifyMark(b.fanotifyFD, unix.FAN_MARK_REMOVE|unix.FAN_MARK_ONLYDIR, b.markMask, unix.AT_FDCWD, markPath) return fmt.Errorf("name_to_handle_at: %w", err) } var st unix.Statfs_t if err := unix.Statfs(markPath, &st); err != nil { _ = unix.FanotifyMark(b.fanotifyFD, unix.FAN_MARK_REMOVE|unix.FAN_MARK_ONLYDIR, b.markMask, unix.AT_FDCWD, markPath) return fmt.Errorf("statfs: %w", err) } key := makeFanotifyHandleKey(st.Fsid.Val, handle.Type(), handle.Bytes()) sub := &fanotifySubscription{path: path, watchPath: markPath, dirWatch: w, key: key} b.subscriptions[key] = append(b.subscriptions[key], sub) return nil } // handleEvents reads and dispatches fanotify events from the fd. func (b *fanotifyBackend) handleEvents() error { buf := b.readBuf watchersTouched := b.watchersTouched for { n, err := unix.Read(b.fanotifyFD, buf) if err != nil { if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) { break } return fmt.Errorf("Error reading from fanotify: %w", err) } if n == 0 { break } metaSize := int(unsafe.Sizeof(unix.FanotifyEventMetadata{})) data := buf[:n] for len(data) >= metaSize { meta := (*unix.FanotifyEventMetadata)(unsafe.Pointer(&data[0])) if meta.Vers != unix.FANOTIFY_METADATA_VERSION { return fmt.Errorf("unsupported fanotify metadata version: %d", meta.Vers) } eventLen := int(meta.Event_len) if eventLen < int(meta.Metadata_len) || eventLen > len(data) { break } // FID mode: fd should be FAN_NOFD, but close if somehow set. if meta.Fd >= 0 { _ = unix.Close(int(meta.Fd)) } if meta.Mask&unix.FAN_Q_OVERFLOW != 0 { b.handleOverflow(watchersTouched) data = data[eventLen:] continue } infoData := data[meta.Metadata_len:eventLen] primary, renameTo := parseFanotifyDfidNames(infoData) if meta.Mask&unix.FAN_RENAME != 0 { if primary != nil || renameTo != nil { b.handleRenameEvent(meta.Mask, primary, renameTo, watchersTouched) } } else if primary != nil { b.handleParsedEvent(meta.Mask, primary, watchersTouched) } data = data[eventLen:] } } for w := range watchersTouched { w.notify() } clear(watchersTouched) return nil } func (b *fanotifyBackend) handleOverflow(touched map[*dirWatch]struct{}) { b.mu.Lock() defer b.mu.Unlock() seen := map[*dirWatch]struct{}{} for _, subs := range b.subscriptions { for _, s := range subs { if _, ok := seen[s.dirWatch]; ok { continue } seen[s.dirWatch] = struct{}{} s.dirWatch.events.setError(ErrOverflow) touched[s.dirWatch] = struct{}{} } } } func (b *fanotifyBackend) handleRenameEvent(mask uint64, dfidOld *fanotifyDfidName, dfidNew *fanotifyDfidName, touched map[*dirWatch]struct{}) { b.mu.Lock() defer b.mu.Unlock() isDir := mask&unix.FAN_ONDIR != 0 // Remove from old location. if dfidOld != nil && dfidOld.name != "" && dfidOld.name != "." { for _, s := range b.subscriptions[dfidOld.key] { oldPath := s.path + "/" + dfidOld.name // If the renamed item is a dir, drop its subscriptions and // all descendant subscriptions. The kernel marks themselves // leak when the destination is outside our watched tree: // fanotify has no path-independent unmark and we don't // keep fds open for marked directories. if isDir { b.dropSubsForPathAndDescendantsLocked(oldPath) } s.dirWatch.events.remove(oldPath) touched[s.dirWatch] = struct{}{} } } // Create at new location. if dfidNew != nil && dfidNew.name != "" && dfidNew.name != "." { for _, s := range b.subscriptions[dfidNew.key] { newPath := s.path + "/" + dfidNew.name s.dirWatch.events.create(newPath) if isDir && s.dirWatch.recursive { _ = walkDir(s.dirWatch.physicalPath(newPath), true, func(p string, pIsDir bool) error { if !pIsDir { return nil } _ = b.markDir(s.dirWatch, s.dirWatch.displayPath(p), p) return nil }) } touched[s.dirWatch] = struct{}{} } } } func (b *fanotifyBackend) handleParsedEvent(mask uint64, dfid *fanotifyDfidName, touched map[*dirWatch]struct{}) { b.mu.Lock() defer b.mu.Unlock() // b.subscriptions[key] holds at most one entry per *fanotifySubscription // pointer (markDir always appends a fresh struct), so no dedup is // necessary. for _, s := range b.subscriptions[dfid.key] { if b.handleSubscription(mask, dfid, s) { touched[s.dirWatch] = struct{}{} } } } func (b *fanotifyBackend) handleSubscription(mask uint64, dfid *fanotifyDfidName, sub *fanotifySubscription) bool { w := sub.dirWatch // Compute full path. Self-events (name empty or ".") use the // watch path directly. isSelfEvent := dfid.name == "" || dfid.name == "." path := sub.path if !isSelfEvent { path = sub.path + "/" + dfid.name } isDir := mask&unix.FAN_ONDIR != 0 touched := false hasDelete := mask&(unix.FAN_DELETE|unix.FAN_MOVED_FROM) != 0 hasCreate := mask&(unix.FAN_CREATE|unix.FAN_MOVED_TO) != 0 // Fanotify can merge consecutive events on the same object into a // single event with multiple mask bits. When both create and delete // bits are set, we can't tell the temporal order from the mask alone. // Stat the path: if it exists, the last op was create (delete→create // = "update"); if gone, the last op was delete (create→delete = // cancel out). if hasCreate && hasDelete && !isSelfEvent { var st unix.Stat_t if unix.Lstat(path, &st) != nil { // File was created then deleted: record both so they cancel. w.events.create(path) w.events.remove(path) return true } // File exists: was deleted then recreated. Fall through to the // normal delete-first processing which produces "update". } // Process delete/move-from FIRST so that a merged DELETE+CREATE // coalesces to "update" via the eventList's rapid-recreate logic. if mask&(unix.FAN_DELETE|unix.FAN_DELETE_SELF|unix.FAN_MOVED_FROM|unix.FAN_MOVE_SELF) != 0 { isSelfMask := mask&(unix.FAN_DELETE_SELF|unix.FAN_MOVE_SELF) != 0 // Ignore delete/move self events unless this is the watch root. if !(isSelfMask && path != w.dir) { // If the deleted/moved item is a dir, drop subscriptions // for both the path itself and every descendant; otherwise // later events for the (now-moved) inodes would be reported // against stale paths. For FAN_MOVED_FROM that takes the // inode out of our watched tree the kernel mark on the // inode itself unfortunately leaks: fanotify has no // path-independent way to unmark and the destination is // outside everything we can resolve. // Self events may not have FAN_ONDIR set (like inotify). if isSelfMask || isDir { b.dropSubsForPathAndDescendantsLocked(path) } else { b.dropSubsForPathLocked(path) } w.events.remove(path) touched = true // Root-of-watch deletion: the kernel has dropped the mark. // Surface ErrWatchTerminated alongside the delete so callers // know to clean up; no more events will arrive for w. if isSelfMask && path == w.dir { w.events.setError(fmt.Errorf("%w: watched directory removed", ErrWatchTerminated)) } } } if hasCreate { w.events.create(path) if isDir && w.recursive { _ = walkDir(w.physicalPath(path), true, func(p string, pIsDir bool) error { if !pIsDir { return nil } _ = b.markDir(w, w.displayPath(p), p) return nil }) } touched = true } if mask&unix.FAN_MODIFY != 0 { w.events.update(path) touched = true } return touched } // parseFanotifyDfidNames extracts DFID_NAME info records from the event's // info record area. Returns a primary record (DFID_NAME or OLD_DFID_NAME) // and an optional second record (NEW_DFID_NAME, for FAN_RENAME events). func parseFanotifyDfidNames(data []byte) (primary *fanotifyDfidName, rename *fanotifyDfidName) { const ( infoHdrSize = 4 // fanotify_event_info_header fsidSize = 8 // __kernel_fsid_t fhHdrSize = 8 // file_handle header (handle_bytes + handle_type) minBodySize = fsidSize + fhHdrSize ) for offset := 0; offset+infoHdrSize <= len(data); { infoType := data[offset] infoLen := int(binary.NativeEndian.Uint16(data[offset+2 : offset+4])) if infoLen < infoHdrSize || offset+infoLen > len(data) { break } switch infoType { case unix.FAN_EVENT_INFO_TYPE_DFID_NAME, unix.FAN_EVENT_INFO_TYPE_OLD_DFID_NAME: if parsed := parseFanotifyFidRecord(data[offset:offset+infoLen], true); parsed != nil { primary = parsed } case unix.FAN_EVENT_INFO_TYPE_NEW_DFID_NAME: if parsed := parseFanotifyFidRecord(data[offset:offset+infoLen], true); parsed != nil { rename = parsed } case unix.FAN_EVENT_INFO_TYPE_DFID: // DFID without name: the handle identifies the directory itself. // Use as fallback if we haven't found a DFID_NAME record. if primary == nil { if parsed := parseFanotifyFidRecord(data[offset:offset+infoLen], false); parsed != nil { primary = parsed } } } if primary != nil && rename != nil { return primary, rename } offset += infoLen } return primary, rename } // parseFanotifyFidRecord parses a single fanotify_event_info_fid record. func parseFanotifyFidRecord(data []byte, hasName bool) *fanotifyDfidName { const ( infoHdrSize = 4 fsidSize = 8 fhHdrSize = 8 minSize = infoHdrSize + fsidSize + fhHdrSize ) if len(data) < minSize { return nil } body := data[infoHdrSize:] var fsid [2]int32 fsid[0] = int32(binary.NativeEndian.Uint32(body[0:4])) fsid[1] = int32(binary.NativeEndian.Uint32(body[4:8])) handleBytes := int(binary.NativeEndian.Uint32(body[8:12])) handleType := int32(binary.NativeEndian.Uint32(body[12:16])) handleStart := fsidSize + fhHdrSize if handleStart+handleBytes > len(body) { return nil } handleData := body[handleStart : handleStart+handleBytes] key := makeFanotifyHandleKey(fsid, handleType, handleData) var name string if hasName { nameStart := handleStart + handleBytes if nameStart < len(body) { nameData := body[nameStart:] for i, c := range nameData { if c == 0 { nameData = nameData[:i] break } } name = string(nameData) } } return &fanotifyDfidName{key: key, name: name} } // dropSubsForPathLocked removes every subscription whose s.path equals // path, regardless of which fanotify handle key it lives under. Must be // called with b.mu held. func (b *fanotifyBackend) dropSubsForPathLocked(path string) { for key, list := range b.subscriptions { kept := list[:0] for _, s := range list { if s.path == path { continue } kept = append(kept, s) } if len(kept) == 0 { delete(b.subscriptions, key) } else { b.subscriptions[key] = kept } } } // dropSubsForPathAndDescendantsLocked removes every subscription whose // s.path equals path or lives strictly under path. The kernel mark on // the moved-out inode itself remains active (fanotify provides no // path-independent unmark) but dropping the bookkeeping prevents later // events from being reported against the no-longer-valid path. // Must be called with b.mu held. func (b *fanotifyBackend) dropSubsForPathAndDescendantsLocked(path string) { for key, list := range b.subscriptions { kept := list[:0] for _, s := range list { if s.path == path || (len(s.path) > len(path) && s.path[len(path)] == '/' && s.path[:len(path)] == path) { continue } kept = append(kept, s) } if len(kept) == 0 { delete(b.subscriptions, key) } else { b.subscriptions[key] = kept } } } func (b *fanotifyBackend) closeWatch(w *dirWatch) error { for key, list := range b.subscriptions { kept := list[:0] removedAny := false var removedPath string for _, s := range list { if s.dirWatch == w { removedAny = true removedPath = s.watchPath continue } kept = append(kept, s) } if !removedAny { continue } if len(kept) == 0 { // Try to unmark. Skip the call entirely when markMask is // still 0 (closeWatch racing with a shutdown that happened // before subscribe ever set markMask); fanotify_mark with // mask=0 is undocumented. Ignore ENOENT (directory may have // been deleted) and EBADF (fanotify fd may already be // closed during shutdown). if b.markMask != 0 { _ = unix.FanotifyMark(b.fanotifyFD, unix.FAN_MARK_REMOVE, b.markMask, unix.AT_FDCWD, removedPath) } delete(b.subscriptions, key) } else { b.subscriptions[key] = kept } } return nil }