Skip to content

Commit 3899270

Browse files
pbnjayarp242
andauthored
Check for errors during FSEventStreamStart (#57)
* Return an error if FSEventStreamStart fails * Validate the 4096 path limit and error detection * Add notes for #46 and #48 limitations of macOS Co-authored-by: Martin Tournoij <[email protected]>
1 parent 0bd000f commit 3899270

4 files changed

Lines changed: 122 additions & 9 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
**Warning:** This API should be considered unstable.
88

9+
## Caveats
10+
11+
Known caveats of the macOS FSEvents API which this package uses under the hood:
12+
13+
- FSEvents returns events for the named path only, so unless you want to follow updates to a symlink itself (unlikely), you should use `filepath.EvalSymlinks` to get the target path to watch.
14+
- There is an internal macOS limitation of 4096 watched paths. Watching more paths will result in an error calling `Start()`. Note that FSEvents is intended to be a recursive watcher by design, it is actually more efficient to watch the containing path than each file in a large directory.
15+
916
## Contributing
1017

1118
Request features and report bugs using the [GitHub Issue Tracker](https://github.com/fsnotify/fsevents/issues).

fsevents.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (r *eventStreamRegistry) Delete(i uintptr) {
136136

137137
// Start listening to an event stream. This creates es.Events if it's not already
138138
// a valid channel.
139-
func (es *EventStream) Start() {
139+
func (es *EventStream) Start() error {
140140
if es.Events == nil {
141141
es.Events = make(chan []Event)
142142
}
@@ -146,7 +146,13 @@ func (es *EventStream) Start() {
146146
cbInfo := registry.Add(es)
147147
es.registryID = cbInfo
148148
es.uuid = GetDeviceUUID(es.Device)
149-
es.start(es.Paths, cbInfo)
149+
err := es.start(es.Paths, cbInfo)
150+
if err != nil {
151+
// Remove eventstream from the registry
152+
registry.Delete(es.registryID)
153+
es.registryID = 0
154+
}
155+
return err
150156
}
151157

152158
// Flush flushes events that have occurred but haven't been delivered.
@@ -170,8 +176,8 @@ func (es *EventStream) Stop() {
170176

171177
// Restart restarts the event listener. This
172178
// can be used to change the current watch flags.
173-
func (es *EventStream) Restart() {
179+
func (es *EventStream) Restart() error {
174180
es.Stop()
175181
es.Resume = true
176-
es.Start()
182+
return es.Start()
177183
}

fsevents_test.go

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package fsevents
55

66
import (
7+
"fmt"
78
"io/ioutil"
89
"os"
910
"path/filepath"
@@ -34,7 +35,10 @@ func TestBasicExample(t *testing.T) {
3435
Flags: FileEvents,
3536
}
3637

37-
es.Start()
38+
err = es.Start()
39+
if err != nil {
40+
t.Fatal(err)
41+
}
3842

3943
wait := make(chan Event)
4044
go func() {
@@ -58,3 +62,90 @@ func TestBasicExample(t *testing.T) {
5862
t.Fatal("timed out waiting for event")
5963
}
6064
}
65+
66+
func TestIssue48(t *testing.T) {
67+
// FSEvents fails to start when watching >4096 paths
68+
// This test validates that limit and checks that the error is propagated
69+
70+
path, err := ioutil.TempDir("", "fsmanyfiles")
71+
if err != nil {
72+
t.Fatal(err)
73+
}
74+
path, err = filepath.EvalSymlinks(path)
75+
if err != nil {
76+
t.Fatal(err)
77+
}
78+
defer os.RemoveAll(path)
79+
80+
// TODO: using this value fails to start
81+
// dev, err := DeviceForPath(path)
82+
// if err != nil {
83+
// t.Fatal(err)
84+
// }
85+
86+
var filenames []string
87+
for i := 0; i < 4096; i++ {
88+
newFilename := filepath.Join(path, fmt.Sprint("test", i))
89+
err = ioutil.WriteFile(newFilename, []byte("test"), 0700)
90+
if err != nil {
91+
t.Fatal(err)
92+
}
93+
filenames = append(filenames, newFilename)
94+
}
95+
96+
es := &EventStream{
97+
Paths: filenames,
98+
Latency: 500 * time.Millisecond,
99+
Device: 0, //dev,
100+
Flags: FileEvents,
101+
}
102+
103+
err = es.Start()
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
108+
wait := make(chan Event)
109+
go func() {
110+
for msg := range es.Events {
111+
for _, event := range msg {
112+
t.Logf("Event: %#v", event)
113+
wait <- event
114+
es.Stop()
115+
return
116+
}
117+
}
118+
}()
119+
120+
// write some new contents to test42 in the watchlist
121+
err = ioutil.WriteFile(filenames[42], []byte("special"), 0700)
122+
if err != nil {
123+
t.Fatal(err)
124+
}
125+
126+
// should be reported as expected
127+
<-wait
128+
129+
/////
130+
// create one more file that puts it over the edge
131+
newFilename := filepath.Join(path, fmt.Sprint("test", 4096))
132+
err = ioutil.WriteFile(newFilename, []byte("test"), 0700)
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
filenames = append(filenames, newFilename)
137+
138+
// create an all-new instances to avoid problems
139+
es = &EventStream{
140+
Paths: filenames,
141+
Latency: 500 * time.Millisecond,
142+
Device: 0, //dev,
143+
Flags: FileEvents,
144+
}
145+
146+
err = es.Start()
147+
if err == nil {
148+
es.Stop()
149+
t.Fatal("eventstream error was not detected on >4096 files in watchlist")
150+
}
151+
}

wrap.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ func setupStream(paths []string, flags CreateFlags, callbackInfo uintptr, eventI
404404
return fsEventStreamRef(ref)
405405
}
406406

407-
func (es *EventStream) start(paths []string, callbackInfo uintptr) {
407+
func (es *EventStream) start(paths []string, callbackInfo uintptr) error {
408408

409409
since := eventIDSinceNow
410410
if es.Resume {
@@ -413,14 +413,23 @@ func (es *EventStream) start(paths []string, callbackInfo uintptr) {
413413

414414
es.stream = setupStream(paths, es.Flags, callbackInfo, since, es.Latency, es.Device)
415415

416-
started := make(chan struct{})
416+
started := make(chan error)
417417

418418
go func() {
419419
runtime.LockOSThread()
420420
es.rlref = cfRunLoopRef(C.CFRunLoopGetCurrent())
421421
C.CFRetain(C.CFTypeRef(es.rlref))
422422
C.FSEventStreamScheduleWithRunLoop(es.stream, C.CFRunLoopRef(es.rlref), C.kCFRunLoopDefaultMode)
423-
C.FSEventStreamStart(es.stream)
423+
if C.FSEventStreamStart(es.stream) == 0 {
424+
// cleanup stream and runloop
425+
C.FSEventStreamInvalidate(es.stream)
426+
C.FSEventStreamRelease(es.stream)
427+
C.CFRelease(C.CFTypeRef(es.rlref))
428+
es.stream = nil
429+
started <- fmt.Errorf("failed to start eventstream")
430+
close(started)
431+
return
432+
}
424433
close(started)
425434
C.CFRunLoopRun()
426435
}()
@@ -432,7 +441,7 @@ func (es *EventStream) start(paths []string, callbackInfo uintptr) {
432441
es.hasFinalizer = true
433442
}
434443

435-
<-started
444+
return <-started
436445
}
437446

438447
func finalizer(es *EventStream) {

0 commit comments

Comments
 (0)