package filenotify import ( "errors" "fmt" "os" "sync" "time" "github.com/Sirupsen/logrus" "gopkg.in/fsnotify.v1" ) var ( // errPollerClosed is returned when the poller is closed errPollerClosed = errors.New("poller is closed") // errNoSuchPoller is returned when trying to remove a watch that doesn't exist errNoSuchWatch = errors.New("poller does not exist") ) // watchWaitTime is the time to wait between file poll loops const watchWaitTime = 200 * time.Millisecond // filePoller is used to poll files for changes, especially in cases where fsnotify // can't be run (e.g. when inotify handles are exhausted) // filePoller satisfies the FileWatcher interface type filePoller struct { // watches is the list of files currently being polled, close the associated channel to stop the watch watches map[string]chan struct{} // events is the channel to listen to for watch events events chan fsnotify.Event // errors is the channel to listen to for watch errors errors chan error // mu locks the poller for modification mu sync.Mutex // closed is used to specify when the poller has already closed closed bool } // Add adds a filename to the list of watches // once added the file is polled for changes in a separate goroutine func (w *filePoller) Add(name string) error { w.mu.Lock() defer w.mu.Unlock() if w.closed == true { return errPollerClosed } f, err := os.Open(name) if err != nil { return err } fi, err := os.Stat(name) if err != nil { return err } if w.watches == nil { w.watches = make(map[string]chan struct{}) } if _, exists := w.watches[name]; exists { return fmt.Errorf("watch exists") } chClose := make(chan struct{}) w.watches[name] = chClose go w.watch(f, fi, chClose) return nil } // Remove stops and removes watch with the specified name func (w *filePoller) Remove(name string) error { w.mu.Lock() defer w.mu.Unlock() return w.remove(name) } func (w *filePoller) remove(name string) error { if w.closed == true { return errPollerClosed } chClose, exists := w.watches[name] if !exists { return errNoSuchWatch } close(chClose) delete(w.watches, name) return nil } // Events returns the event channel // This is used for notifications on events about watched files func (w *filePoller) Events() <-chan fsnotify.Event { return w.events } // Errors returns the errors channel // This is used for notifications about errors on watched files func (w *filePoller) Errors() <-chan error { return w.errors } // Close closes the poller // All watches are stopped, removed, and the poller cannot be added to func (w *filePoller) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.closed { return nil } w.closed = true for name := range w.watches { w.remove(name) delete(w.watches, name) } close(w.events) close(w.errors) return nil } // sendEvent publishes the specified event to the events channel func (w *filePoller) sendEvent(e fsnotify.Event, chClose <-chan struct{}) error { select { case w.events <- e: case <-chClose: return fmt.Errorf("closed") } return nil } // sendErr publishes the specified error to the errors channel func (w *filePoller) sendErr(e error, chClose <-chan struct{}) error { select { case w.errors <- e: case <-chClose: return fmt.Errorf("closed") } return nil } // watch is responsible for polling the specified file for changes // upon finding changes to a file or errors, sendEvent/sendErr is called func (w *filePoller) watch(f *os.File, lastFi os.FileInfo, chClose chan struct{}) { for { time.Sleep(watchWaitTime) select { case <-chClose: logrus.Debugf("watch for %s closed", f.Name()) return default: } fi, err := os.Stat(f.Name()) if err != nil { // if we got an error here and lastFi is not set, we can presume that nothing has changed // This should be safe since before `watch()` is called, a stat is performed, there is any error `watch` is not called if lastFi == nil { continue } // If it doesn't exist at this point, it must have been removed // no need to send the error here since this is a valid operation if os.IsNotExist(err) { if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Remove, Name: f.Name()}, chClose); err != nil { return } lastFi = nil continue } // at this point, send the error if err := w.sendErr(err, chClose); err != nil { return } continue } if lastFi == nil { if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Create, Name: fi.Name()}, chClose); err != nil { return } lastFi = fi continue } if fi.Mode() != lastFi.Mode() { if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Chmod, Name: fi.Name()}, chClose); err != nil { return } lastFi = fi continue } if fi.ModTime() != lastFi.ModTime() || fi.Size() != lastFi.Size() { if err := w.sendEvent(fsnotify.Event{Op: fsnotify.Write, Name: fi.Name()}, chClose); err != nil { return } lastFi = fi continue } } }