// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
- Events chan Event
- Errors chan error
- mu sync.Mutex // Map access
- fd int // File descriptor (as returned by the inotify_init() syscall)
- watches map[string]*watch // Map of inotify watches (key: path)
- paths map[int]string // Map of watched paths (key: watch descriptor)
- done chan bool // Channel for sending a "quit message" to the reader goroutine
- isClosed bool // Set to true when Close() is first called
+ Events chan Event
+ Errors chan error
+ mu sync.Mutex // Map access
+ fd int // File descriptor (as returned by the inotify_init() syscall)
+ watches map[string]*watch // Map of inotify watches (key: path)
+ paths map[int]string // Map of watched paths (key: watch descriptor)
+ done chan struct{} // Channel for sending a "quit message" to the reader goroutine
}
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
paths: make(map[int]string),
Events: make(chan Event),
Errors: make(chan error),
- done: make(chan bool, 1),
+ done: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
+func (w *Watcher) isClosed() bool {
+ select {
+ case <-w.done:
+ return true
+ default:
+ return false
+ }
+}
+
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
- if w.isClosed {
+ if w.isClosed() {
return nil
}
- w.isClosed = true
- // Remove all watches
- for name := range w.watches {
- w.Remove(name)
- }
+ // Send 'close' signal to goroutine, and set the Watcher to closed.
+ close(w.done)
- // Send "quit" message to the reader goroutine
- w.done <- true
+ // Remove all watches.
+ // Everything after this may generate errors because the inotify channel
+ // has been closed; we don't care.
+ numWatches := w.removeAll()
+
+ // If no watches were removed, it's possible syscall.Read is still blocking.
+ // In this case, create a watch and remove it to wake it up.
+ // If that fails, there's really nothing left to do. we've done our best,
+ // but the goroutine may be alive forever.
+ if numWatches == 0 {
+ wd, _ := syscall.InotifyAddWatch(w.fd, ".", syscall.IN_DELETE_SELF)
+ syscall.InotifyRmWatch(w.fd, uint32(wd))
+ }
return nil
}
// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
name = filepath.Clean(name)
- if w.isClosed {
+ if w.isClosed() {
return errors.New("inotify instance already closed")
}
return nil
}
-// Remove stops watching the the named file or directory (non-recursively).
+// Remove stops watching the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name)
+
+ // Fetch the watch.
w.mu.Lock()
defer w.mu.Unlock()
watch, ok := w.watches[name]
+
+ // Remove it from inotify.
if !ok {
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name)
}
return nil
}
+// removeAll watches
+func (w *Watcher) removeAll() int {
+ removed := 0
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ for name, watch := range w.watches {
+ success, _ := syscall.InotifyRmWatch(w.fd, watch.wd)
+ if success != -1 {
+ removed++
+ }
+ delete(w.watches, name)
+ }
+ return removed
+}
+
type watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
errno error // Syscall errno
)
+ defer close(w.Errors)
+ defer close(w.Events)
+ defer syscall.Close(w.fd)
+
for {
- // See if there is a message on the "done" channel
- select {
- case <-w.done:
- syscall.Close(w.fd)
- close(w.Events)
- close(w.Errors)
+ // See if we have been closed.
+ if w.isClosed() {
return
- default:
}
n, errno = syscall.Read(w.fd, buf[:])
continue
}
+ // syscall.Read might have been woken up by Close. If so, we're done.
+ if w.isClosed() {
+ return
+ }
+
// If EOF is received
if n == 0 {
- syscall.Close(w.fd)
- close(w.Events)
- close(w.Errors)
+ close(w.done)
return
}
if n < 0 {
- w.Errors <- os.NewSyscallError("read", errno)
+ select {
+ case w.Errors <- os.NewSyscallError("read", errno):
+ case <-w.done:
+ return
+ }
continue
}
if n < syscall.SizeofInotifyEvent {
- w.Errors <- errors.New("inotify: short read in readEvents()")
+ select {
+ case w.Errors <- errors.New("inotify: short read in readEvents()"):
+ case <-w.done:
+ return
+ }
continue
}
// Send the events that are not ignored on the events channel
if !event.ignoreLinux(mask) {
- w.Events <- event
+ select {
+ case w.Events <- event:
+ case <-w.done:
+ return
+ }
}
// Move to the next event in the buffer
--- /dev/null
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build linux
+
+package fsnotify
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestInotifyCloseRightAway(t *testing.T) {
+ w, err := NewWatcher()
+ if err != nil {
+ t.Fatalf("Failed to create watcher")
+ }
+
+ // Close immediately; it won't even reach the first syscall.Read.
+ w.Close()
+
+ // Wait for the close to complete.
+ <-time.After(50 * time.Millisecond)
+ isWatcherReallyClosed(t, w)
+}
+
+func TestInotifyCloseSlightlyLater(t *testing.T) {
+ w, err := NewWatcher()
+ if err != nil {
+ t.Fatalf("Failed to create watcher")
+ }
+
+ // Wait until readEvents has reached syscall.Read, and Close.
+ <-time.After(50 * time.Millisecond)
+ w.Close()
+
+ // Wait for the close to complete.
+ <-time.After(50 * time.Millisecond)
+ isWatcherReallyClosed(t, w)
+}
+
+func TestInotifyCloseSlightlyLaterWithWatch(t *testing.T) {
+ testDir := tempMkdir(t)
+ defer os.RemoveAll(testDir)
+
+ w, err := NewWatcher()
+ if err != nil {
+ t.Fatalf("Failed to create watcher")
+ }
+ w.Add(testDir)
+
+ // Wait until readEvents has reached syscall.Read, and Close.
+ <-time.After(50 * time.Millisecond)
+ w.Close()
+
+ // Wait for the close to complete.
+ <-time.After(50 * time.Millisecond)
+ isWatcherReallyClosed(t, w)
+}
+
+func TestInotifyCloseAfterRead(t *testing.T) {
+ testDir := tempMkdir(t)
+ defer os.RemoveAll(testDir)
+
+ w, err := NewWatcher()
+ if err != nil {
+ t.Fatalf("Failed to create watcher")
+ }
+
+ err = w.Add(testDir)
+ if err != nil {
+ t.Fatalf("Failed to add .")
+ }
+
+ // Generate an event.
+ os.Create(filepath.Join(testDir, "somethingSOMETHINGsomethingSOMETHING"))
+
+ // Wait for readEvents to read the event, then close the watcher.
+ <-time.After(50 * time.Millisecond)
+ w.Close()
+
+ // Wait for the close to complete.
+ <-time.After(50 * time.Millisecond)
+ isWatcherReallyClosed(t, w)
+}
+
+func isWatcherReallyClosed(t *testing.T, w *Watcher) {
+ select {
+ case err, ok := <-w.Errors:
+ if ok {
+ t.Fatalf("w.Errors is not closed; readEvents is still alive after closing (error: %v)", err)
+ }
+ default:
+ t.Fatalf("w.Errors would have blocked; readEvents is still alive!")
+ }
+
+ select {
+ case _, ok := <-w.Events:
+ if ok {
+ t.Fatalf("w.Events is not closed; readEvents is still alive after closing")
+ }
+ default:
+ t.Fatalf("w.Events would have blocked; readEvents is still alive!")
+ }
+}