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 |
+} |