vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,784 @@
//go:build darwin || freebsd || openbsd || netbsd || dragonfly
package fswatch
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
"golang.org/x/sys/unix"
)
// ---------------------------------------------------------------------------
// kqueue.go: kqueue backend (macOS, FreeBSD, OpenBSD, NetBSD, DragonFlyBSD)
//
// Uses the kernel's kqueue/kevent mechanism to watch individual files and
// directories via EVFILT_VNODE. Unlike inotify, kqueue requires an open file
// descriptor per watched path, not just per directory. On macOS, O_EVTONLY
// opens files for event monitoring only; on other BSDs, O_RDONLY is used.
//
// ┌──────────────────────────────────────────────────────────────┐
// │ kqueueBackend │
// │ │
// │ ┌───────────┐ kevent(2) ┌──────────────────┐ │
// │ │ pipe[0] ├───────────────────────►│ │ │
// │ │ (wakeup) │ │ start() │ │
// │ └───────────┘ │ goroutine │ │
// │ ┌───────────┐ EVFILT_VNODE │ (event loop) │ │
// │ │ kqueue ├───────────────────────►│ │ │
// │ │ fd │ └────────┬─────────┘ │
// │ └───────────┘ │ │
// │ ▼ │
// │ ┌──────────────────────────────────────────────┐ │
// │ │ fdToEntry: map[fd] → *dirEntry │ │
// │ │ subsByPath: map[path] → []*kqueueSub │ │
// │ │ │ │
// │ │ Each dirEntry.state stores the open fd │ │
// │ └──────────────────────────────────────────────┘ │
// └──────────────────────────────────────────────────────────────┘
//
// Goroutines and threading:
// - One long-lived goroutine (start), launched by watcherBase.run(). It
// owns the kevent(2) loop and runs for the process lifetime. All event
// dispatch (compareDir, handleFileEvent) executes on this goroutine.
// compareDir and handleFileEvent acquire b.mu for watch/fd lookups.
// - subscribe/closeWatch run on the caller's goroutine under
// watcherBase.mu. watchPath acquires b.mu to register fd mappings.
//
// 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:
// 1. Walk the target directory, building a path→dirEntry map (caller goroutine).
// 2. For every entry (file or directory), open an fd and register it with
// kqueue for EVFILT_VNODE events (NOTE_DELETE, NOTE_WRITE, NOTE_EXTEND,
// NOTE_ATTRIB, NOTE_RENAME, NOTE_REVOKE). Store the fd↔dirEntry mapping.
//
// Event dispatch (on the start goroutine):
// - NOTE_WRITE on a directory → compareDir: re-read the directory from
// disk, diff against the in-memory tree, emit update events for new
// entries (opening + watching them) and delete events for removed ones
// (closing their fds).
// - NOTE_DELETE / NOTE_RENAME / NOTE_REVOKE → close the stale fd. For a
// pure NOTE_DELETE on a file, tryRewatchLocked checks whether the path
// was immediately recreated (atomic-save pattern) and emits update
// instead of delete if so. Otherwise emit delete and remove from the
// tree. Directories skip tryRewatchLocked to avoid spurious updates
// during RemoveAll races.
// - NOTE_WRITE / NOTE_ATTRIB / NOTE_EXTEND on a file → emit update.
// After processing all returned kevents, call dirWatch.notify() on each
// touched dirWatch to trigger the debouncer.
//
// Shutdown:
// Write a byte to pipe[1] → kevent sees the pipe fd → loop exits →
// close all tracked fds, the kqueue fd, and the pipe.
// ---------------------------------------------------------------------------
// openForEvents opens a path for kqueue event monitoring. On darwin, O_EVTONLY
// opens the file for event notification without granting read access. On other
// BSDs, falls back to O_RDONLY.
func openForEvents(path string) (int, error) {
flags := unix.O_RDONLY
if runtime.GOOS == "darwin" {
flags = 0x8000 // O_EVTONLY, darwin-only
}
return unix.Open(path, flags, 0)
}
// dirEntry tracks a watched path for kqueue's fd↔path mapping.
type dirEntry struct {
path string
watchPath string
isDir bool
state any // stores the open fd
}
// kqueueSubscription.
type kqueueSubscription struct {
dirWatch *dirWatch
path string
entries map[string]*dirEntry
fd int
}
// kqueueBackend. It embeds treeReaderBackend (via Go
// composition) just like the inheritance hierarchy.
type kqueueBackend struct {
watcherBase
mu sync.Mutex // local lock for kqueue-specific maps
kq int
// pipeFDs[0] is read in the Start goroutine only. pipeFDs[1] is written
// by Shutdown (any goroutine) to wake the loop, so it lives in
// pipeWriteFD as an atomic with a sentinel of -1 once closed.
pipeFDs [2]int
pipeWriteFD atomic.Int32
subsByPath map[string][]*kqueueSubscription // multimap<path, sub>
fdToEntry map[int]*dirEntry
endedSignal chan struct{}
// Persistent buffer reused across event batches. Only accessed
// from the start goroutine, so no synchronization needed.
watchersTouched map[*dirWatch]struct{}
}
func init() {
kqueueWatcher.factory = func() watcherImpl { return newKqueueBackend() }
}
func newKqueueBackend() *kqueueBackend {
b := &kqueueBackend{
kq: -1,
pipeFDs: [2]int{-1, -1},
subsByPath: map[string][]*kqueueSubscription{},
fdToEntry: map[int]*dirEntry{},
endedSignal: make(chan struct{}),
watchersTouched: make(map[*dirWatch]struct{}),
}
b.pipeWriteFD.Store(-1)
b.watcherBase.init(b)
return b
}
func (b *kqueueBackend) start() error {
kq, err := unix.Kqueue()
if err != nil {
return fmt.Errorf("unable to open kqueue: %w", err)
}
b.kq = kq
defer func() {
b.closeSubscriptions()
b.closeFDs()
close(b.endedSignal)
}()
if err := unix.Pipe(b.pipeFDs[:]); err != nil {
return fmt.Errorf("unable to open pipe: %w", err)
}
b.pipeWriteFD.Store(int32(b.pipeFDs[1]))
// WatchDirectory kqueue to the read side of the pipe so we can break the
// loop on shutdown. SetKevent handles the per-arch Ident type
// (uint64 on 64-bit, uint32 on 386/arm).
var pipeEv unix.Kevent_t
unix.SetKevent(&pipeEv, b.pipeFDs[0], unix.EVFILT_READ, unix.EV_ADD|unix.EV_CLEAR)
if _, err := unix.Kevent(kq, []unix.Kevent_t{pipeEv}, nil, nil); err != nil {
return fmt.Errorf("unable to watch pipe: %w", err)
}
b.notifyStarted()
events := make([]unix.Kevent_t, 128)
for {
n, err := unix.Kevent(kq, nil, events, nil)
if err != nil {
if err == unix.EINTR {
continue
}
return fmt.Errorf("kevent error: %w", err)
}
watchersTouched := b.watchersTouched
stop := false
for i := range n {
fflags := events[i].Fflags
flags := events[i].Flags
fd := int(events[i].Ident)
if fd == b.pipeFDs[0] {
stop = true
break
}
// EV_ERROR indicates kevent couldn't apply a changelist
// entry or that the kernel rejected the registration.
// Data carries the errno. Skip dispatching as a normal
// event since fflags are not meaningful in this case.
if flags&unix.EV_ERROR != 0 {
continue
}
b.mu.Lock()
entry, ok := b.fdToEntry[fd]
b.mu.Unlock()
if !ok || entry == nil {
continue
}
if fflags&unix.NOTE_WRITE != 0 && entry.isDir {
b.compareDir(fd, entry.path, watchersTouched)
// NOTE_WRITE on a dir already ran compareDir above.
// On DragonFlyBSD, rename-over coalesces NOTE_DELETE
// with NOTE_WRITE on the parent directory (rather than
// firing NOTE_DELETE on the replaced file's fd).
// Skip handleFileEvent so we don't misinterpret the
// coalesced NOTE_DELETE as the directory itself being
// removed.
fflags &^= unix.NOTE_DELETE
}
if fflags&^unix.NOTE_WRITE != 0 || !entry.isDir {
b.handleFileEvent(fflags, entry, watchersTouched)
}
}
for w := range watchersTouched {
w.notify()
}
clear(watchersTouched)
if stop {
break
}
}
return nil
}
func (b *kqueueBackend) closeFDs() {
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.kq >= 0 {
_ = unix.Close(b.kq)
b.kq = -1
}
}
func (b *kqueueBackend) closeSubscriptions() {
b.mu.Lock()
seenFDs := map[int]struct{}{}
for _, list := range b.subsByPath {
for _, sub := range list {
if sub.fd < 0 {
continue
}
if _, ok := seenFDs[sub.fd]; ok {
continue
}
seenFDs[sub.fd] = struct{}{}
_ = unix.Close(sub.fd)
}
}
b.subsByPath = map[string][]*kqueueSubscription{}
b.fdToEntry = map[int]*dirEntry{}
b.mu.Unlock()
}
func (b *kqueueBackend) shutdown() {
fd := b.pipeWriteFD.Load()
if fd < 0 {
return
}
_, _ = unix.Write(int(fd), []byte{'X'})
<-b.endedSignal
}
func (b *kqueueBackend) handleFileEvent(fflags uint32, entry *dirEntry, touched map[*dirWatch]struct{}) {
b.mu.Lock()
defer b.mu.Unlock()
subs := b.findSubscriptionsLocked(entry.path)
if fflags&(unix.NOTE_DELETE|unix.NOTE_RENAME|unix.NOTE_REVOKE) != 0 {
// Close the stale fd; the watched inode is gone.
if oldFD, ok := entry.state.(int); ok {
unix.Close(oldFD)
delete(b.fdToEntry, oldFD)
entry.state = nil
}
recreated := false
if fflags&unix.NOTE_DELETE != 0 && fflags&(unix.NOTE_RENAME|unix.NOTE_REVOKE) == 0 && !entry.isDir {
recreated = b.tryRewatchLocked(entry)
}
for _, sub := range subs {
touched[sub.dirWatch] = struct{}{}
if recreated {
sub.dirWatch.events.update(sub.path)
} else {
sub.dirWatch.events.remove(sub.path)
// If we lost a directory, walk the entries map and
// close every fd we had open for descendants. Some
// kernels (OpenBSD in particular) deliver only the
// parent's NOTE_DELETE/NOTE_RENAME and never fire
// NOTE_DELETE on the children; without this cleanup,
// modifying a file inside the moved tree later
// surfaces an event against the descendant's stale
// (pre-rename) path. We also emit a delete for each
// descendant we close, so callers don't miss those
// removals if the kernel didn't fire per-child events.
// (When the kernel does fire them, our follow-up
// handleFileEvent finds the fd already gone and is a
// no-op, so events.create's coalescing handles dups.)
if entry.isDir {
b.closeDescendantFDsLocked(sub.dirWatch, sub.entries, sub.path)
}
removeEntryAndDescendants(sub.entries, sub.path)
// Root-of-watch deletion: no more events can fire
// for this dirWatch. Tell the caller.
if sub.path == sub.dirWatch.dir {
sub.dirWatch.events.setError(fmt.Errorf("%w: watched directory removed", ErrWatchTerminated))
}
}
}
if !recreated {
delete(b.subsByPath, entry.path)
}
return
}
for _, sub := range subs {
touched[sub.dirWatch] = struct{}{}
if fflags&(unix.NOTE_WRITE|unix.NOTE_ATTRIB|unix.NOTE_EXTEND) != 0 {
sub.dirWatch.events.update(sub.path)
}
}
}
// closeDescendantFDsLocked closes every fd attached to an entry whose
// path lives strictly under root, removing the kevent registration and
// the corresponding b.subsByPath / b.fdToEntry bookkeeping, and emits a
// delete event for each. Used when a directory's parent is lost
// (deleted, renamed away) and the kernel didn't propagate the loss to
// children. eventList coalesces against any per-child NOTE_DELETE that
// arrives later.
func (b *kqueueBackend) closeDescendantFDsLocked(w *dirWatch, entries map[string]*dirEntry, root string) {
prefix := root + string(filepath.Separator)
for path, e := range entries {
if !strings.HasPrefix(path, prefix) {
continue
}
if fd, ok := e.state.(int); ok {
unix.Close(fd)
delete(b.fdToEntry, fd)
e.state = nil
}
delete(b.subsByPath, path)
w.events.remove(path)
}
}
// tryRewatchLocked checks whether a deleted path was immediately recreated
// with the same type. If so, it opens a new fd, registers a kqueue watch,
// and returns true. The caller should emit update instead of delete.
func (b *kqueueBackend) tryRewatchLocked(entry *dirEntry) bool {
var st unix.Stat_t
if unix.Lstat(entry.watchPath, &st) != nil {
return false
}
// Only fast-path when the recreated path has the same type;
// a file→dir change needs a full tree rebuild via compareDir.
newIsDir := st.Mode&unix.S_IFMT == unix.S_IFDIR
if newIsDir != entry.isDir {
return false
}
fd, err := openForEvents(entry.watchPath)
if err != nil {
return false
}
var ev unix.Kevent_t
unix.SetKevent(&ev, fd, unix.EVFILT_VNODE, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE)
ev.Fflags = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_EXTEND |
unix.NOTE_ATTRIB | unix.NOTE_RENAME | unix.NOTE_REVOKE
if _, err := unix.Kevent(b.kq, []unix.Kevent_t{ev}, nil, nil); err != nil {
unix.Close(fd)
return false
}
entry.state = fd
b.fdToEntry[fd] = entry
return true
}
func (b *kqueueBackend) closeEntryLocked(entry *dirEntry) {
if fd, ok := entry.state.(int); ok {
unix.Close(fd)
delete(b.fdToEntry, fd)
entry.state = nil
}
}
func (b *kqueueBackend) removeSubsForEntriesLocked(path string, entriesPtr *map[string]*dirEntry) {
list := b.subsByPath[path]
kept := list[:0]
for _, sub := range list {
if &sub.entries == entriesPtr {
continue
}
kept = append(kept, sub)
}
if len(kept) == 0 {
delete(b.subsByPath, path)
} else {
b.subsByPath[path] = kept
}
}
func (b *kqueueBackend) removeEntryAndDescendantsLocked(entriesPtr *map[string]*dirEntry, path string, includeRoot bool) {
entries := *entriesPtr
for descendant, e := range entries {
if descendant == path {
if !includeRoot {
continue
}
} else if !(len(descendant) > len(path) && descendant[len(path)] == filepath.Separator && descendant[:len(path)] == path) {
continue
}
b.closeEntryLocked(e)
b.removeSubsForEntriesLocked(descendant, entriesPtr)
delete(entries, descendant)
}
}
func (b *kqueueBackend) findSubscriptionsLocked(path string) []*kqueueSubscription {
subs := b.subsByPath[path]
out := make([]*kqueueSubscription, len(subs))
copy(out, subs)
return out
}
// subscribe mirrors `kqueueBackend::subscribe`. Called under watcherBase.mu
// via watchAdd.
func (b *kqueueBackend) subscribe(w *dirWatch) error {
// Build the entries map without registering any watches or
// subscriptions. This avoids a data race: registering a subscription
// publishes the entries map to the event loop (via subsByPath),
// which could read it via compareDir while we're still populating it.
entries := map[string]*dirEntry{}
if err := walkDir(w.physicalDir, w.recursive, func(watchPath string, isDir bool) error {
path := w.displayPath(watchPath)
entries[path] = &dirEntry{path: path, watchPath: watchPath, isDir: isDir}
return nil
}); err != nil {
return err
}
// Open fds, register kevents, and publish subscriptions under b.mu.
// Holding the lock for the entire block ensures that the event loop
// cannot see a partially-built entries map, and that fds are always
// tracked in fdToEntry (no leak on early return).
b.mu.Lock()
defer b.mu.Unlock()
for path, entry := range entries {
fd, err := openForEvents(entry.watchPath)
if err != nil {
if path == w.dir {
b.cleanupEntriesLocked(entries)
return &dirWatchError{
err: fmt.Errorf("error watching %s: %w", w.dir, err),
dirWatch: w,
}
}
delete(entries, path)
continue
}
var ev unix.Kevent_t
unix.SetKevent(&ev, fd, unix.EVFILT_VNODE, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE)
ev.Fflags = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_EXTEND |
unix.NOTE_ATTRIB | unix.NOTE_RENAME | unix.NOTE_REVOKE
if _, err := unix.Kevent(b.kq, []unix.Kevent_t{ev}, nil, nil); err != nil {
unix.Close(fd)
if path == w.dir {
b.cleanupEntriesLocked(entries)
return &dirWatchError{
err: fmt.Errorf("error watching %s: %w", w.dir, err),
dirWatch: w,
}
}
delete(entries, path)
continue
}
entry.state = fd
b.fdToEntry[fd] = entry
}
for path, entry := range entries {
fd := entry.state.(int)
sub := &kqueueSubscription{dirWatch: w, path: path, entries: entries, fd: fd}
b.subsByPath[path] = append(b.subsByPath[path], sub)
}
return nil
}
// cleanupEntriesLocked closes fds for all entries that have been opened.
// Called on subscribe failure to avoid fd leaks. Must be called under b.mu.
func (b *kqueueBackend) cleanupEntriesLocked(entries map[string]*dirEntry) {
for _, e := range entries {
if fd, ok := e.state.(int); ok {
unix.Close(fd)
delete(b.fdToEntry, fd)
e.state = nil
}
}
}
// watchPath corresponds to `kqueueBackend::watchDir`.
func (b *kqueueBackend) watchPath(w *dirWatch, path string, entries map[string]*dirEntry) bool {
entry := entries[path]
if entry == nil {
return false
}
b.mu.Lock()
defer b.mu.Unlock()
sub := &kqueueSubscription{dirWatch: w, path: path, entries: entries}
if entry.state == nil {
fd, err := openForEvents(entry.watchPath)
if err != nil {
return false
}
var ev unix.Kevent_t
unix.SetKevent(&ev, fd, unix.EVFILT_VNODE, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE)
ev.Fflags = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_EXTEND |
unix.NOTE_ATTRIB | unix.NOTE_RENAME | unix.NOTE_REVOKE
if _, err := unix.Kevent(b.kq, []unix.Kevent_t{ev}, nil, nil); err != nil {
unix.Close(fd)
return false
}
entry.state = fd
b.fdToEntry[fd] = entry
}
sub.fd = entry.state.(int)
b.subsByPath[path] = append(b.subsByPath[path], sub)
return true
}
// compareDir mirrors `kqueueBackend::compareDir`. Triggered when a watched
// directory has NOTE_WRITE: list the dir, diff against the tree, emit
// create/remove events.
func (b *kqueueBackend) compareDir(_ int, path string, touched map[*dirWatch]struct{}) bool {
b.mu.Lock()
subs := b.findSubscriptionsLocked(path)
b.mu.Unlock()
// For non-recursive subscriptions, only compareDir on the root dir.
// NOTE_WRITE on a child dir means something changed inside it, but
// non-recursive mode shouldn't report those changes. Emit an update
// for the child dir itself (its metadata changed) and return.
filteredSubs := subs[:0:0]
for _, s := range subs {
if !s.dirWatch.recursive && path != s.dirWatch.dir {
s.dirWatch.events.update(path)
touched[s.dirWatch] = struct{}{}
} else {
filteredSubs = append(filteredSubs, s)
}
}
if len(filteredSubs) == 0 {
return true
}
subs = filteredSubs
dirStart := path + string(filepath.Separator)
type diskSnapshot struct {
entries []os.DirEntry
currentDisplayPaths map[string]struct{}
}
snapshots := map[string]diskSnapshot{}
// Each subscription has its own entries map (built in subscribe).
// Multiple subs at the same path arise from multiple dirWatches
// covering overlapping subtrees; their maps are always distinct, so
// we iterate subs directly rather than trying to dedup by map identity.
for _, sub := range subs {
baseEntry := sub.entries[path]
if baseEntry == nil {
continue
}
watchPath := baseEntry.watchPath
watchDirStart := watchPath + string(filepath.Separator)
snapshot, ok := snapshots[watchPath]
if !ok {
diskEntries, err := readEntries(watchPath)
if err != nil {
continue
}
snapshot.entries = diskEntries
snapshot.currentDisplayPaths = make(map[string]struct{}, len(diskEntries))
for _, ent := range diskEntries {
snapshot.currentDisplayPaths[dirStart+ent.Name()] = struct{}{}
}
snapshots[watchPath] = snapshot
}
entries := sub.entries
for _, ent := range snapshot.entries {
fullPath := dirStart + ent.Name()
fullWatchPath := watchDirStart + ent.Name()
existing := entries[fullPath]
if existing != nil {
if existing.state != nil {
// Check if the fd still refers to the same inode as
// the path on disk. On DragonFlyBSD, rename-over
// doesn't fire NOTE_DELETE on the replaced file's fd,
// leaving a stale entry whose fd points to the old
// (now unlinked) inode.
if fd, ok := existing.state.(int); ok {
var fdSt, pathSt unix.Stat_t
if unix.Fstat(fd, &fdSt) == nil && unix.Lstat(fullWatchPath, &pathSt) == nil {
if fdSt.Dev != pathSt.Dev || fdSt.Ino != pathSt.Ino {
// Inode changed: path was replaced.
b.mu.Lock()
b.closeEntryLocked(existing)
b.removeSubsForEntriesLocked(fullPath, &sub.entries)
if existing.isDir {
b.removeEntryAndDescendantsLocked(&sub.entries, fullPath, false)
}
existing.isDir = ent.IsDir()
b.mu.Unlock()
}
}
}
}
if existing.state != nil {
continue
}
// Entry exists but fd is stale: the file was replaced.
// Re-watch it and emit an update.
if !b.watchPath(sub.dirWatch, fullPath, entries) {
continue
}
sub.dirWatch.events.update(fullPath)
touched[sub.dirWatch] = struct{}{}
if ent.IsDir() && sub.dirWatch.recursive {
_ = walkDir(fullWatchPath, true, func(p string, pIsDir bool) error {
if p == fullWatchPath {
return nil
}
displayPath := sub.dirWatch.displayPath(p)
e := &dirEntry{path: displayPath, watchPath: p, isDir: pIsDir}
entries[displayPath] = e
sub.dirWatch.events.create(displayPath)
b.watchPath(sub.dirWatch, displayPath, entries)
return nil
})
}
continue
}
e := &dirEntry{path: fullPath, watchPath: fullWatchPath, isDir: ent.IsDir()}
entries[fullPath] = e
if !b.watchPath(sub.dirWatch, fullPath, entries) {
delete(entries, fullPath)
continue
}
sub.dirWatch.events.create(fullPath)
touched[sub.dirWatch] = struct{}{}
// For recursive subscriptions, walk into the new directory
// to catch pre-populated subdirectories (e.g. a directory
// tree moved into the watched area).
if ent.IsDir() && sub.dirWatch.recursive {
_ = walkDir(fullWatchPath, true, func(p string, pIsDir bool) error {
if p == fullWatchPath {
return nil // already handled above
}
displayPath := sub.dirWatch.displayPath(p)
entry := &dirEntry{path: displayPath, watchPath: p, isDir: pIsDir}
entries[displayPath] = entry
sub.dirWatch.events.create(displayPath)
b.watchPath(sub.dirWatch, displayPath, entries)
return nil
})
}
}
// Detect removals: entries directly under dirStart that no longer
// exist on disk.
var toRemove []string
for p := range entries {
if !strings.HasPrefix(p, dirStart) {
continue
}
rest := p[len(dirStart):]
if strings.Contains(rest, string(filepath.Separator)) {
continue
}
if _, ok := snapshot.currentDisplayPaths[p]; ok {
continue
}
toRemove = append(toRemove, p)
}
for _, p := range toRemove {
sub.dirWatch.events.remove(p)
touched[sub.dirWatch] = struct{}{}
b.mu.Lock()
for descendant, e := range entries {
if descendant != p && !(len(descendant) > len(p) && descendant[len(p)] == filepath.Separator && descendant[:len(p)] == p) {
continue
}
if fd, ok := e.state.(int); ok {
unix.Close(fd)
delete(b.fdToEntry, fd)
}
delete(b.subsByPath, descendant)
}
b.mu.Unlock()
removeEntryAndDescendants(entries, p)
}
}
return true
}
// readEntries lists directory entries (excluding "." and "..") at path.
func readEntries(path string) ([]os.DirEntry, error) {
return os.ReadDir(path)
}
// closeWatch mirrors `kqueueBackend::closeWatch`.
func (b *kqueueBackend) closeWatch(w *dirWatch) error {
b.mu.Lock()
defer b.mu.Unlock()
for path, list := range b.subsByPath {
kept := list[:0]
removedAny := false
for _, s := range list {
if s.dirWatch == w {
removedAny = true
continue
}
kept = append(kept, s)
}
if !removedAny {
continue
}
if len(kept) == 0 {
// Closing the file descriptor automatically unwatches it in kqueue.
fd := list[0].fd
unix.Close(fd)
delete(b.fdToEntry, fd)
delete(b.subsByPath, path)
} else {
b.subsByPath[path] = kept
}
}
return nil
}
// removeEntryAndDescendants removes path and all paths prefixed with
// path + separator from the entries map.
func removeEntryAndDescendants(entries map[string]*dirEntry, path string) {
delete(entries, path)
for k := range entries {
if len(k) > len(path) && k[len(path)] == filepath.Separator && k[:len(path)] == path {
delete(entries, k)
}
}
}