This reverts commit ee99b5f2e96aafa982487aadbb78478898ae0c71.
Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,40 @@ |
0 |
+// Package filenotify provides a mechanism for watching file(s) for changes. |
|
1 |
+// Generally leans on fsnotify, but provides a poll-based notifier which fsnotify does not support. |
|
2 |
+// These are wrapped up in a common interface so that either can be used interchangeably in your code. |
|
3 |
+package filenotify |
|
4 |
+ |
|
5 |
+import "gopkg.in/fsnotify.v1" |
|
6 |
+ |
|
7 |
+// FileWatcher is an interface for implementing file notification watchers |
|
8 |
+type FileWatcher interface { |
|
9 |
+ Events() <-chan fsnotify.Event |
|
10 |
+ Errors() <-chan error |
|
11 |
+ Add(name string) error |
|
12 |
+ Remove(name string) error |
|
13 |
+ Close() error |
|
14 |
+} |
|
15 |
+ |
|
16 |
+// New tries to use an fs-event watcher, and falls back to the poller if there is an error |
|
17 |
+func New() (FileWatcher, error) { |
|
18 |
+ if watcher, err := NewEventWatcher(); err == nil { |
|
19 |
+ return watcher, nil |
|
20 |
+ } |
|
21 |
+ return NewPollingWatcher(), nil |
|
22 |
+} |
|
23 |
+ |
|
24 |
+// NewPollingWatcher returns a poll-based file watcher |
|
25 |
+func NewPollingWatcher() FileWatcher { |
|
26 |
+ return &filePoller{ |
|
27 |
+ events: make(chan fsnotify.Event), |
|
28 |
+ errors: make(chan error), |
|
29 |
+ } |
|
30 |
+} |
|
31 |
+ |
|
32 |
+// NewEventWatcher returns an fs-event based file watcher |
|
33 |
+func NewEventWatcher() (FileWatcher, error) { |
|
34 |
+ watcher, err := fsnotify.NewWatcher() |
|
35 |
+ if err != nil { |
|
36 |
+ return nil, err |
|
37 |
+ } |
|
38 |
+ return &fsNotifyWatcher{watcher}, nil |
|
39 |
+} |
0 | 40 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,18 @@ |
0 |
+package filenotify |
|
1 |
+ |
|
2 |
+import "gopkg.in/fsnotify.v1" |
|
3 |
+ |
|
4 |
+// fsNotify wraps the fsnotify package to satisfy the FileNotifer interface |
|
5 |
+type fsNotifyWatcher struct { |
|
6 |
+ *fsnotify.Watcher |
|
7 |
+} |
|
8 |
+ |
|
9 |
+// GetEvents returns the fsnotify event channel receiver |
|
10 |
+func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event { |
|
11 |
+ return w.Watcher.Events |
|
12 |
+} |
|
13 |
+ |
|
14 |
+// GetErrors returns the fsnotify error channel receiver |
|
15 |
+func (w *fsNotifyWatcher) Errors() <-chan error { |
|
16 |
+ return w.Watcher.Errors |
|
17 |
+} |
0 | 18 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,205 @@ |
0 |
+package filenotify |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "errors" |
|
4 |
+ "fmt" |
|
5 |
+ "os" |
|
6 |
+ "sync" |
|
7 |
+ "time" |
|
8 |
+ |
|
9 |
+ "github.com/Sirupsen/logrus" |
|
10 |
+ |
|
11 |
+ "gopkg.in/fsnotify.v1" |
|
12 |
+) |
|
13 |
+ |
|
14 |
+var ( |
|
15 |
+ // errPollerClosed is returned when the poller is closed |
|
16 |
+ errPollerClosed = errors.New("poller is closed") |
|
17 |
+ // errNoSuchPoller is returned when trying to remove a watch that doesn't exist |
|
18 |
+ errNoSuchWatch = errors.New("poller does not exist") |
|
19 |
+) |
|
20 |
+ |
|
21 |
+// watchWaitTime is the time to wait between file poll loops |
|
22 |
+const watchWaitTime = 200 * time.Millisecond |
|
23 |
+ |
|
24 |
+// filePoller is used to poll files for changes, especially in cases where fsnotify |
|
25 |
+// can't be run (e.g. when inotify handles are exhausted) |
|
26 |
+// filePoller satisfies the FileWatcher interface |
|
27 |
+type filePoller struct { |
|
28 |
+ // watches is the list of files currently being polled, close the associated channel to stop the watch |
|
29 |
+ watches map[string]chan struct{} |
|
30 |
+ // events is the channel to listen to for watch events |
|
31 |
+ events chan fsnotify.Event |
|
32 |
+ // errors is the channel to listen to for watch errors |
|
33 |
+ errors chan error |
|
34 |
+ // mu locks the poller for modification |
|
35 |
+ mu sync.Mutex |
|
36 |
+ // closed is used to specify when the poller has already closed |
|
37 |
+ closed bool |
|
38 |
+} |
|
39 |
+ |
|
40 |
+// Add adds a filename to the list of watches |
|
41 |
+// once added the file is polled for changes in a separate goroutine |
|
42 |
+func (w *filePoller) Add(name string) error { |
|
43 |
+ w.mu.Lock() |
|
44 |
+ defer w.mu.Unlock() |
|
45 |
+ |
|
46 |
+ if w.closed == true { |
|
47 |
+ return errPollerClosed |
|
48 |
+ } |
|
49 |
+ |
|
50 |
+ f, err := os.Open(name) |
|
51 |
+ if err != nil { |
|
52 |
+ return err |
|
53 |
+ } |
|
54 |
+ fi, err := os.Stat(name) |
|
55 |
+ if err != nil { |
|
56 |
+ return err |
|
57 |
+ } |
|
58 |
+ |
|
59 |
+ if w.watches == nil { |
|
60 |
+ w.watches = make(map[string]chan struct{}) |
|
61 |
+ } |
|
62 |
+ if _, exists := w.watches[name]; exists { |
|
63 |
+ return fmt.Errorf("watch exists") |
|
64 |
+ } |
|
65 |
+ chClose := make(chan struct{}) |
|
66 |
+ w.watches[name] = chClose |
|
67 |
+ |
|
68 |
+ go w.watch(f, fi, chClose) |
|
69 |
+ return nil |
|
70 |
+} |
|
71 |
+ |
|
72 |
+// Remove stops and removes watch with the specified name |
|
73 |
+func (w *filePoller) Remove(name string) error { |
|
74 |
+ w.mu.Lock() |
|
75 |
+ defer w.mu.Unlock() |
|
76 |
+ return w.remove(name) |
|
77 |
+} |
|
78 |
+ |
|
79 |
+func (w *filePoller) remove(name string) error { |
|
80 |
+ if w.closed == true { |
|
81 |
+ return errPollerClosed |
|
82 |
+ } |
|
83 |
+ |
|
84 |
+ chClose, exists := w.watches[name] |
|
85 |
+ if !exists { |
|
86 |
+ return errNoSuchWatch |
|
87 |
+ } |
|
88 |
+ close(chClose) |
|
89 |
+ delete(w.watches, name) |
|
90 |
+ return nil |
|
91 |
+} |
|
92 |
+ |
|
93 |
+// Events returns the event channel |
|
94 |
+// This is used for notifications on events about watched files |
|
95 |
+func (w *filePoller) Events() <-chan fsnotify.Event { |
|
96 |
+ return w.events |
|
97 |
+} |
|
98 |
+ |
|
99 |
+// Errors returns the errors channel |
|
100 |
+// This is used for notifications about errors on watched files |
|
101 |
+func (w *filePoller) Errors() <-chan error { |
|
102 |
+ return w.errors |
|
103 |
+} |
|
104 |
+ |
|
105 |
+// Close closes the poller |
|
106 |
+// All watches are stopped, removed, and the poller cannot be added to |
|
107 |
+func (w *filePoller) Close() error { |
|
108 |
+ w.mu.Lock() |
|
109 |
+ defer w.mu.Unlock() |
|
110 |
+ |
|
111 |
+ if w.closed { |
|
112 |
+ return nil |
|
113 |
+ } |
|
114 |
+ |
|
115 |
+ w.closed = true |
|
116 |
+ for name := range w.watches { |
|
117 |
+ w.remove(name) |
|
118 |
+ delete(w.watches, name) |
|
119 |
+ } |
|
120 |
+ close(w.events) |
|
121 |
+ close(w.errors) |
|
122 |
+ return nil |
|
123 |
+} |
|
124 |
+ |
|
125 |
+// sendEvent publishes the specified event to the events channel |
|
126 |
+func (w *filePoller) sendEvent(e fsnotify.Event, chClose <-chan struct{}) error { |
|
127 |
+ select { |
|
128 |
+ case w.events <- e: |
|
129 |
+ case <-chClose: |
|
130 |
+ return fmt.Errorf("closed") |
|
131 |
+ } |
|
132 |
+ return nil |
|
133 |
+} |
|
134 |
+ |
|
135 |
+// sendErr publishes the specified error to the errors channel |
|
136 |
+func (w *filePoller) sendErr(e error, chClose <-chan struct{}) error { |
|
137 |
+ select { |
|
138 |
+ case w.errors <- e: |
|
139 |
+ case <-chClose: |
|
140 |
+ return fmt.Errorf("closed") |
|
141 |
+ } |
|
142 |
+ return nil |
|
143 |
+} |
|
144 |
+ |
|
145 |
+// watch is responsible for polling the specified file for changes |
|
146 |
+// upon finding changes to a file or errors, sendEvent/sendErr is called |
|
147 |
+func (w *filePoller) watch(f *os.File, lastFi os.FileInfo, chClose chan struct{}) { |
|
148 |
+ for { |
|
149 |
+ time.Sleep(watchWaitTime) |
|
150 |
+ select { |
|
151 |
+ case <-chClose: |
|
152 |
+ logrus.Debugf("watch for %s closed", f.Name()) |
|
153 |
+ return |
|
154 |
+ default: |
|
155 |
+ } |
|
156 |
+ |
|
157 |
+ fi, err := os.Stat(f.Name()) |
|
158 |
+ if err != nil { |
|
159 |
+ // if we got an error here and lastFi is not set, we can presume that nothing has changed |
|
160 |
+ // This should be safe since before `watch()` is called, a stat is performed, there is any error `watch` is not called |
|
161 |
+ if lastFi == nil { |
|
162 |
+ continue |
|
163 |
+ } |
|
164 |
+ // If it doesn't exist at this point, it must have been removed |
|
165 |
+ // no need to send the error here since this is a valid operation |
|
166 |
+ if os.IsNotExist(err) { |
|
167 |
+ if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Remove, Name: f.Name()}, chClose); err != nil { |
|
168 |
+ return |
|
169 |
+ } |
|
170 |
+ lastFi = nil |
|
171 |
+ continue |
|
172 |
+ } |
|
173 |
+ // at this point, send the error |
|
174 |
+ if err := w.sendErr(err, chClose); err != nil { |
|
175 |
+ return |
|
176 |
+ } |
|
177 |
+ continue |
|
178 |
+ } |
|
179 |
+ |
|
180 |
+ if lastFi == nil { |
|
181 |
+ if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: fi.Name()}, chClose); err != nil { |
|
182 |
+ return |
|
183 |
+ } |
|
184 |
+ lastFi = fi |
|
185 |
+ continue |
|
186 |
+ } |
|
187 |
+ |
|
188 |
+ if fi.Mode() != lastFi.Mode() { |
|
189 |
+ if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Chmod, Name: fi.Name()}, chClose); err != nil { |
|
190 |
+ return |
|
191 |
+ } |
|
192 |
+ lastFi = fi |
|
193 |
+ continue |
|
194 |
+ } |
|
195 |
+ |
|
196 |
+ if fi.ModTime() != lastFi.ModTime() || fi.Size() != lastFi.Size() { |
|
197 |
+ if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Write, Name: fi.Name()}, chClose); err != nil { |
|
198 |
+ return |
|
199 |
+ } |
|
200 |
+ lastFi = fi |
|
201 |
+ continue |
|
202 |
+ } |
|
203 |
+ } |
|
204 |
+} |
0 | 205 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,137 @@ |
0 |
+package filenotify |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "fmt" |
|
4 |
+ "io/ioutil" |
|
5 |
+ "os" |
|
6 |
+ "runtime" |
|
7 |
+ "testing" |
|
8 |
+ "time" |
|
9 |
+ |
|
10 |
+ "gopkg.in/fsnotify.v1" |
|
11 |
+) |
|
12 |
+ |
|
13 |
+func TestPollerAddRemove(t *testing.T) { |
|
14 |
+ w := NewPollingWatcher() |
|
15 |
+ |
|
16 |
+ if err := w.Add("no-such-file"); err == nil { |
|
17 |
+ t.Fatal("should have gotten error when adding a non-existent file") |
|
18 |
+ } |
|
19 |
+ if err := w.Remove("no-such-file"); err == nil { |
|
20 |
+ t.Fatal("should have gotten error when removing non-existent watch") |
|
21 |
+ } |
|
22 |
+ |
|
23 |
+ f, err := ioutil.TempFile("", "asdf") |
|
24 |
+ if err != nil { |
|
25 |
+ t.Fatal(err) |
|
26 |
+ } |
|
27 |
+ defer os.RemoveAll(f.Name()) |
|
28 |
+ |
|
29 |
+ if err := w.Add(f.Name()); err != nil { |
|
30 |
+ t.Fatal(err) |
|
31 |
+ } |
|
32 |
+ |
|
33 |
+ if err := w.Remove(f.Name()); err != nil { |
|
34 |
+ t.Fatal(err) |
|
35 |
+ } |
|
36 |
+} |
|
37 |
+ |
|
38 |
+func TestPollerEvent(t *testing.T) { |
|
39 |
+ if runtime.GOOS == "windows" { |
|
40 |
+ t.Skip("No chmod on Windows") |
|
41 |
+ } |
|
42 |
+ w := NewPollingWatcher() |
|
43 |
+ |
|
44 |
+ f, err := ioutil.TempFile("", "test-poller") |
|
45 |
+ if err != nil { |
|
46 |
+ t.Fatal("error creating temp file") |
|
47 |
+ } |
|
48 |
+ defer os.RemoveAll(f.Name()) |
|
49 |
+ f.Close() |
|
50 |
+ |
|
51 |
+ if err := w.Add(f.Name()); err != nil { |
|
52 |
+ t.Fatal(err) |
|
53 |
+ } |
|
54 |
+ |
|
55 |
+ select { |
|
56 |
+ case <-w.Events(): |
|
57 |
+ t.Fatal("got event before anything happened") |
|
58 |
+ case <-w.Errors(): |
|
59 |
+ t.Fatal("got error before anything happened") |
|
60 |
+ default: |
|
61 |
+ } |
|
62 |
+ |
|
63 |
+ if err := ioutil.WriteFile(f.Name(), []byte("hello"), 644); err != nil { |
|
64 |
+ t.Fatal(err) |
|
65 |
+ } |
|
66 |
+ if err := assertEvent(w, fsnotify.Write); err != nil { |
|
67 |
+ t.Fatal(err) |
|
68 |
+ } |
|
69 |
+ |
|
70 |
+ if err := os.Chmod(f.Name(), 600); err != nil { |
|
71 |
+ t.Fatal(err) |
|
72 |
+ } |
|
73 |
+ if err := assertEvent(w, fsnotify.Chmod); err != nil { |
|
74 |
+ t.Fatal(err) |
|
75 |
+ } |
|
76 |
+ |
|
77 |
+ if err := os.Remove(f.Name()); err != nil { |
|
78 |
+ t.Fatal(err) |
|
79 |
+ } |
|
80 |
+ if err := assertEvent(w, fsnotify.Remove); err != nil { |
|
81 |
+ t.Fatal(err) |
|
82 |
+ } |
|
83 |
+} |
|
84 |
+ |
|
85 |
+func TestPollerClose(t *testing.T) { |
|
86 |
+ w := NewPollingWatcher() |
|
87 |
+ if err := w.Close(); err != nil { |
|
88 |
+ t.Fatal(err) |
|
89 |
+ } |
|
90 |
+ // test double-close |
|
91 |
+ if err := w.Close(); err != nil { |
|
92 |
+ t.Fatal(err) |
|
93 |
+ } |
|
94 |
+ |
|
95 |
+ select { |
|
96 |
+ case _, open := <-w.Events(): |
|
97 |
+ if open { |
|
98 |
+ t.Fatal("event chan should be closed") |
|
99 |
+ } |
|
100 |
+ default: |
|
101 |
+ t.Fatal("event chan should be closed") |
|
102 |
+ } |
|
103 |
+ |
|
104 |
+ select { |
|
105 |
+ case _, open := <-w.Errors(): |
|
106 |
+ if open { |
|
107 |
+ t.Fatal("errors chan should be closed") |
|
108 |
+ } |
|
109 |
+ default: |
|
110 |
+ t.Fatal("errors chan should be closed") |
|
111 |
+ } |
|
112 |
+ |
|
113 |
+ f, err := ioutil.TempFile("", "asdf") |
|
114 |
+ if err != nil { |
|
115 |
+ t.Fatal(err) |
|
116 |
+ } |
|
117 |
+ defer os.RemoveAll(f.Name()) |
|
118 |
+ if err := w.Add(f.Name()); err == nil { |
|
119 |
+ t.Fatal("should have gotten error adding watch for closed watcher") |
|
120 |
+ } |
|
121 |
+} |
|
122 |
+ |
|
123 |
+func assertEvent(w FileWatcher, eType fsnotify.Op) error { |
|
124 |
+ var err error |
|
125 |
+ select { |
|
126 |
+ case e := <-w.Events(): |
|
127 |
+ if e.Op != eType { |
|
128 |
+ err = fmt.Errorf("got wrong event type, expected %q: %v", eType, e) |
|
129 |
+ } |
|
130 |
+ case e := <-w.Errors(): |
|
131 |
+ err = fmt.Errorf("got unexpected error waiting for events %v: %v", eType, e) |
|
132 |
+ case <-time.After(watchWaitTime * 3): |
|
133 |
+ err = fmt.Errorf("timeout waiting for event %v", eType) |
|
134 |
+ } |
|
135 |
+ return err |
|
136 |
+} |