Skip to content

Commit 0ef9f7c

Browse files
committed
kqueue: add non-recursive watches
Add Watcher.AddNonRecursive for raw kqueue event masks without the directory child-watching behavior used to mimic inotify. Track non-recursive directory watches so directory NOTE_WRITE events are delivered directly instead of triggering child scans later. This is useful for device directories such as /dev where opening existing entries can fail with EBUSY. Validation: go test -run TestAddNonRecursiveDoesNotWatchChildren; go test -run TestRemoveState
1 parent 788a996 commit 0ef9f7c

2 files changed

Lines changed: 77 additions & 28 deletions

File tree

‎backend_kqueue.go‎

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,18 @@ type Watcher struct {
124124
// Errors sends any errors.
125125
Errors chan error
126126

127-
done chan struct{}
128-
kq int // File descriptor (as returned by the kqueue() syscall).
129-
closepipe [2]int // Pipe used for closing.
130-
mu sync.Mutex // Protects access to watcher data
131-
watches map[string]int // Watched file descriptors (key: path).
132-
watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
133-
userWatches map[string]struct{} // Watches added with Watcher.Add()
134-
dirFlags map[string]uint32 // Watched directories to fflags used in kqueue.
135-
paths map[int]pathInfo // File descriptors to path names for processing kqueue events.
136-
fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events).
137-
isClosed bool // Set to true when Close() is first called
127+
done chan struct{}
128+
kq int // File descriptor (as returned by the kqueue() syscall).
129+
closepipe [2]int // Pipe used for closing.
130+
mu sync.Mutex // Protects access to watcher data
131+
watches map[string]int // Watched file descriptors (key: path).
132+
watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
133+
userWatches map[string]struct{} // Watches added with Watcher.Add()
134+
dirFlags map[string]uint32 // Watched directories to fflags used in kqueue.
135+
nonRecursiveDirs map[string]struct{} // Watched directories that should not watch children.
136+
paths map[int]pathInfo // File descriptors to path names for processing kqueue events.
137+
fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events).
138+
isClosed bool // Set to true when Close() is first called
138139
}
139140

140141
type pathInfo struct {
@@ -162,17 +163,18 @@ func NewBufferedWatcher(sz uint) (*Watcher, error) {
162163
}
163164

164165
w := &Watcher{
165-
kq: kq,
166-
closepipe: closepipe,
167-
watches: make(map[string]int),
168-
watchesByDir: make(map[string]map[int]struct{}),
169-
dirFlags: make(map[string]uint32),
170-
paths: make(map[int]pathInfo),
171-
fileExists: make(map[string]struct{}),
172-
userWatches: make(map[string]struct{}),
173-
Events: make(chan Event, sz),
174-
Errors: make(chan error),
175-
done: make(chan struct{}),
166+
kq: kq,
167+
closepipe: closepipe,
168+
watches: make(map[string]int),
169+
watchesByDir: make(map[string]map[int]struct{}),
170+
dirFlags: make(map[string]uint32),
171+
nonRecursiveDirs: make(map[string]struct{}),
172+
paths: make(map[int]pathInfo),
173+
fileExists: make(map[string]struct{}),
174+
userWatches: make(map[string]struct{}),
175+
Events: make(chan Event, sz),
176+
Errors: make(chan error),
177+
done: make(chan struct{}),
176178
}
177179

178180
go w.readEvents()
@@ -313,15 +315,23 @@ func (w *Watcher) AddWith(name string, opts ...addOpt) error {
313315
w.mu.Lock()
314316
w.userWatches[name] = struct{}{}
315317
w.mu.Unlock()
316-
_, err := w.addWatch(name, noteAllEvents)
318+
_, err := w.addWatch(name, noteAllEvents, true)
317319
return err
318320
}
319321

320322
func (w *Watcher) AddWithEvents(name string, flags uint32) error {
321323
w.mu.Lock()
322324
w.userWatches[name] = struct{}{}
323325
w.mu.Unlock()
324-
_, err := w.addWatch(name, flags)
326+
_, err := w.addWatch(name, flags, true)
327+
return err
328+
}
329+
330+
func (w *Watcher) AddNonRecursive(name string, flags uint32) error {
331+
w.mu.Lock()
332+
w.userWatches[name] = struct{}{}
333+
w.mu.Unlock()
334+
_, err := w.addWatch(name, flags, false)
325335
return err
326336
}
327337

@@ -371,6 +381,7 @@ func (w *Watcher) remove(name string, unwatchFiles bool) error {
371381

372382
delete(w.paths, watchfd)
373383
delete(w.dirFlags, name)
384+
delete(w.nonRecursiveDirs, name)
374385
delete(w.fileExists, name)
375386
w.mu.Unlock()
376387

@@ -421,7 +432,7 @@ const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | un
421432
// described in kevent(2).
422433
//
423434
// Returns the real path to the file which was added, with symlinks resolved.
424-
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
435+
func (w *Watcher) addWatch(name string, flags uint32, recursive bool) (string, error) {
425436
var isDir bool
426437
name = filepath.Clean(name)
427438

@@ -522,9 +533,15 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
522533
w.mu.Lock()
523534

524535
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
536+
recursive &&
525537
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE)
526538
// Store flags so this watch can be updated later
527539
w.dirFlags[name] = flags
540+
if recursive {
541+
delete(w.nonRecursiveDirs, name)
542+
} else {
543+
w.nonRecursiveDirs[name] = struct{}{}
544+
}
528545
w.mu.Unlock()
529546

530547
if watchDir {
@@ -584,7 +601,11 @@ func (w *Watcher) readEvents() {
584601
w.mu.Unlock()
585602
}
586603

587-
if path.isDir && event.Has(Write) && !event.Has(Remove) {
604+
w.mu.Lock()
605+
_, nonRecursive := w.nonRecursiveDirs[event.Name]
606+
w.mu.Unlock()
607+
608+
if path.isDir && event.Has(Write) && !event.Has(Remove) && !nonRecursive {
588609
w.sendDirectoryChangeEvents(event.Name)
589610
} else {
590611
if !w.sendEvent(event) {
@@ -752,11 +773,11 @@ func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
752773
w.mu.Unlock()
753774

754775
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME
755-
return w.addWatch(name, flags)
776+
return w.addWatch(name, flags, true)
756777
}
757778

758779
// watch file to mimic Linux inotify
759-
return w.addWatch(name, noteAllEvents)
780+
return w.addWatch(name, noteAllEvents, true)
760781
}
761782

762783
// Register events with the queue.

‎backend_kqueue_test.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88
"strings"
99
"testing"
10+
11+
"golang.org/x/sys/unix"
1012
)
1113

1214
func TestRemoveState(t *testing.T) {
@@ -94,3 +96,29 @@ func TestRemoveState(t *testing.T) {
9496
}
9597
}
9698
}
99+
100+
func TestAddNonRecursiveDoesNotWatchChildren(t *testing.T) {
101+
var (
102+
tmp = t.TempDir()
103+
dir = join(tmp, "dir")
104+
file = join(dir, "file")
105+
)
106+
mkdir(t, dir)
107+
touch(t, file)
108+
109+
w := newWatcher(t)
110+
defer w.Close()
111+
if err := w.AddNonRecursive(dir, unix.NOTE_WRITE); err != nil {
112+
t.Fatal(err)
113+
}
114+
115+
if len(w.watches) != 1 {
116+
t.Fatalf("unexpected number of entries in w.watches: have %d, want 1", len(w.watches))
117+
}
118+
if _, ok := w.watches[file]; ok {
119+
t.Fatalf("child file %q should not be watched", file)
120+
}
121+
if _, ok := w.nonRecursiveDirs[dir]; !ok {
122+
t.Fatalf("directory %q should be marked non-recursive", dir)
123+
}
124+
}

0 commit comments

Comments
 (0)