diff --git a/fsnotify/fsnotify.go b/fsnotify/fsnotify.go index f37196029..6d1ccbeb3 100644 --- a/fsnotify/fsnotify.go +++ b/fsnotify/fsnotify.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package fsnotify implements filesystem notification. +// Package fsnotify implements file system notification. package fsnotify import "fmt" @@ -102,6 +102,10 @@ func (e *FileEvent) String() string { events += "|" + "RENAME" } + if e.IsAttrib() { + events += "|" + "ATTRIB" + } + if len(events) > 0 { events = events[1:] } diff --git a/fsnotify/fsnotify_bsd.go b/fsnotify/fsnotify_bsd.go index bd4d9d80c..7fb24af80 100644 --- a/fsnotify/fsnotify_bsd.go +++ b/fsnotify/fsnotify_bsd.go @@ -39,41 +39,48 @@ type FileEvent struct { create bool // set by fsnotify package if found new file } -// IsCreate reports whether the FileEvent was triggerd by a creation +// IsCreate reports whether the FileEvent was triggered by a creation func (e *FileEvent) IsCreate() bool { return e.create } -// IsDelete reports whether the FileEvent was triggerd by a delete +// IsDelete reports whether the FileEvent was triggered by a delete func (e *FileEvent) IsDelete() bool { return (e.mask & sys_NOTE_DELETE) == sys_NOTE_DELETE } -// IsModify reports whether the FileEvent was triggerd by a file modification +// IsModify reports whether the FileEvent was triggered by a file modification func (e *FileEvent) IsModify() bool { return ((e.mask&sys_NOTE_WRITE) == sys_NOTE_WRITE || (e.mask&sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB) } -// IsRename reports whether the FileEvent was triggerd by a change name +// IsRename reports whether the FileEvent was triggered by a change name func (e *FileEvent) IsRename() bool { return (e.mask & sys_NOTE_RENAME) == sys_NOTE_RENAME } +// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata. +func (e *FileEvent) IsAttrib() bool { + return (e.mask & sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB +} + type Watcher struct { - mu sync.Mutex // Mutex for the Watcher itself. - kq int // File descriptor (as returned by the kqueue() syscall) - watches map[string]int // Map of watched file diescriptors (key: path) - wmut sync.Mutex // Protects access to watches. - fsnFlags map[string]uint32 // Map of watched files to flags used for filter - fsnmut sync.Mutex // Protects access to fsnFlags. - enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue - enmut sync.Mutex // Protects access to enFlags. - paths map[int]string // Map of watched paths (key: watch descriptor) - finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor) - pmut sync.Mutex // Protects access to paths and finfo. - fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events) - femut sync.Mutex // Proctects access to fileExists. - Error chan error // Errors are sent on this channel - internalEvent chan *FileEvent // Events are queued on this channel - Event chan *FileEvent // Events are returned on this channel - done chan bool // Channel for sending a "quit message" to the reader goroutine - isClosed bool // Set to true when Close() is first called - kbuf [1]syscall.Kevent_t // An event buffer for Add/Remove watch - bufmut sync.Mutex // Protects access to kbuf. + mu sync.Mutex // Mutex for the Watcher itself. + kq int // File descriptor (as returned by the kqueue() syscall) + watches map[string]int // Map of watched file descriptors (key: path) + wmut sync.Mutex // Protects access to watches. + fsnFlags map[string]uint32 // Map of watched files to flags used for filter + fsnmut sync.Mutex // Protects access to fsnFlags. + enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue + enmut sync.Mutex // Protects access to enFlags. + paths map[int]string // Map of watched paths (key: watch descriptor) + finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor) + pmut sync.Mutex // Protects access to paths and finfo. + fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events) + femut sync.Mutex // Protects access to fileExists. + externalWatches map[string]bool // Map of watches added by user of the library. + ewmut sync.Mutex // Protects access to externalWatches. + Error chan error // Errors are sent on this channel + internalEvent chan *FileEvent // Events are queued on this channel + Event chan *FileEvent // Events are returned on this channel + done chan bool // Channel for sending a "quit message" to the reader goroutine + isClosed bool // Set to true when Close() is first called + kbuf [1]syscall.Kevent_t // An event buffer for Add/Remove watch + bufmut sync.Mutex // Protects access to kbuf. } // NewWatcher creates and returns a new kevent instance using kqueue(2) @@ -83,17 +90,18 @@ func NewWatcher() (*Watcher, error) { return nil, os.NewSyscallError("kqueue", errno) } w := &Watcher{ - kq: fd, - watches: make(map[string]int), - fsnFlags: make(map[string]uint32), - enFlags: make(map[string]uint32), - paths: make(map[int]string), - finfo: make(map[int]os.FileInfo), - fileExists: make(map[string]bool), - internalEvent: make(chan *FileEvent), - Event: make(chan *FileEvent), - Error: make(chan error), - done: make(chan bool, 1), + kq: fd, + watches: make(map[string]int), + fsnFlags: make(map[string]uint32), + enFlags: make(map[string]uint32), + paths: make(map[int]string), + finfo: make(map[int]os.FileInfo), + fileExists: make(map[string]bool), + externalWatches: make(map[string]bool), + internalEvent: make(chan *FileEvent), + Event: make(chan *FileEvent), + Error: make(chan error), + done: make(chan bool, 1), } go w.readEvents() @@ -224,6 +232,9 @@ func (w *Watcher) addWatch(path string, flags uint32) error { // Watch adds path to the watched file set, watching all events. func (w *Watcher) watch(path string) error { + w.ewmut.Lock() + w.externalWatches[path] = true + w.ewmut.Unlock() return w.addWatch(path, sys_NOTE_ALLEVENTS) } @@ -236,10 +247,10 @@ func (w *Watcher) removeWatch(path string) error { return errors.New(fmt.Sprintf("can't remove non-existent kevent watch for: %s", path)) } w.bufmut.Lock() - defer w.bufmut.Unlock() watchEntry := &w.kbuf[0] syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE) success, errno := syscall.Kevent(w.kq, w.kbuf[:], nil, nil) + w.bufmut.Unlock() if success == -1 { return os.NewSyscallError("kevent_rm_watch", errno) } else if (watchEntry.Flags & syscall.EV_ERROR) == syscall.EV_ERROR { @@ -254,8 +265,33 @@ func (w *Watcher) removeWatch(path string) error { w.enmut.Unlock() w.pmut.Lock() delete(w.paths, watchfd) + fInfo := w.finfo[watchfd] delete(w.finfo, watchfd) w.pmut.Unlock() + + // Find all watched paths that are in this directory that are not external. + if fInfo.IsDir() { + var pathsToRemove []string + w.pmut.Lock() + for _, wpath := range w.paths { + wdir, _ := filepath.Split(wpath) + if filepath.Clean(wdir) == filepath.Clean(path) { + w.ewmut.Lock() + if !w.externalWatches[wpath] { + pathsToRemove = append(pathsToRemove, wpath) + } + w.ewmut.Unlock() + } + } + w.pmut.Unlock() + for _, p := range pathsToRemove { + // Since these are internal, not much sense in propagating error + // to the user, as that will just confuse them with an error about + // a path they did not explicitly watch themselves. + w.removeWatch(p) + } + } + return nil } @@ -309,7 +345,7 @@ func (w *Watcher) readEvents() { } } - // Flush the events we recieved to the events channel + // Flush the events we received to the events channel for len(events) > 0 { fileEvent := new(FileEvent) watchEvent := &events[0] @@ -318,7 +354,7 @@ func (w *Watcher) readEvents() { fileEvent.Name = w.paths[int(watchEvent.Ident)] fileInfo := w.finfo[int(watchEvent.Ident)] w.pmut.Unlock() - if fileInfo.IsDir() && !fileEvent.IsDelete() { + if fileInfo != nil && fileInfo.IsDir() && !fileEvent.IsDelete() { // Double check to make sure the directory exist. This can happen when // we do a rm -fr on a recursively watched folders and we receive a // modification event first but the folder has been deleted and later @@ -329,7 +365,7 @@ func (w *Watcher) readEvents() { } } - if fileInfo.IsDir() && fileEvent.IsModify() && !fileEvent.IsDelete() { + if fileInfo != nil && fileInfo.IsDir() && fileEvent.IsModify() && !fileEvent.IsDelete() { w.sendDirectoryChangeEvents(fileEvent.Name) } else { // Send the event on the events channel @@ -399,7 +435,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error { return e } } else { - // If the user is currently waching directory + // If the user is currently watching directory // we want to preserve the flags used w.enmut.Lock() currFlags, found := w.enFlags[filePath] @@ -425,7 +461,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error { // sendDirectoryEvents searches the directory for newly created files // and sends them over the event channel. This functionality is to have -// the BSD version of fsnotify mach linux fsnotify which provides a +// the BSD version of fsnotify match linux fsnotify which provides a // create event for files created in a watched directory. func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { // Get all files diff --git a/fsnotify/fsnotify_linux.go b/fsnotify/fsnotify_linux.go index d74b8e54b..80ade879f 100644 --- a/fsnotify/fsnotify_linux.go +++ b/fsnotify/fsnotify_linux.go @@ -62,26 +62,31 @@ type FileEvent struct { Name string // File name (optional) } -// IsCreate reports whether the FileEvent was triggerd by a creation +// IsCreate reports whether the FileEvent was triggered by a creation func (e *FileEvent) IsCreate() bool { return (e.mask&sys_IN_CREATE) == sys_IN_CREATE || (e.mask&sys_IN_MOVED_TO) == sys_IN_MOVED_TO } -// IsDelete reports whether the FileEvent was triggerd by a delete +// IsDelete reports whether the FileEvent was triggered by a delete func (e *FileEvent) IsDelete() bool { return (e.mask&sys_IN_DELETE_SELF) == sys_IN_DELETE_SELF || (e.mask&sys_IN_DELETE) == sys_IN_DELETE } -// IsModify reports whether the FileEvent was triggerd by a file modification or attribute change +// IsModify reports whether the FileEvent was triggered by a file modification or attribute change func (e *FileEvent) IsModify() bool { return ((e.mask&sys_IN_MODIFY) == sys_IN_MODIFY || (e.mask&sys_IN_ATTRIB) == sys_IN_ATTRIB) } -// IsRename reports whether the FileEvent was triggerd by a change name +// IsRename reports whether the FileEvent was triggered by a change name func (e *FileEvent) IsRename() bool { return ((e.mask&sys_IN_MOVE_SELF) == sys_IN_MOVE_SELF || (e.mask&sys_IN_MOVED_FROM) == sys_IN_MOVED_FROM) } +// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata. +func (e *FileEvent) IsAttrib() bool { + return (e.mask & sys_IN_ATTRIB) == sys_IN_ATTRIB +} + 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) @@ -201,16 +206,20 @@ func (w *Watcher) readEvents() { ) for { - n, errno = syscall.Read(w.fd, buf[0:]) // See if there is a message on the "done" channel - var done bool select { - case done = <-w.done: + case <-w.done: + syscall.Close(w.fd) + close(w.internalEvent) + close(w.Error) + return default: } - // If EOF or a "done" message is received - if n == 0 || done { + n, errno = syscall.Read(w.fd, buf[:]) + + // If EOF is received + if n == 0 { syscall.Close(w.fd) close(w.internalEvent) close(w.Error) diff --git a/fsnotify/fsnotify_symlink_test.go b/fsnotify/fsnotify_symlink_test.go index 795e9230a..39061f844 100644 --- a/fsnotify/fsnotify_symlink_test.go +++ b/fsnotify/fsnotify_symlink_test.go @@ -8,23 +8,16 @@ package fsnotify import ( "os" + "path/filepath" "testing" "time" ) func TestFsnotifyFakeSymlink(t *testing.T) { - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } - - const testDir string = "_test" + watcher := newWatcher(t) // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("Failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) var errorsReceived counter @@ -37,8 +30,7 @@ func TestFsnotifyFakeSymlink(t *testing.T) { }() // Count the CREATE events received - var createEventsReceived counter - var otherEventsReceived counter + var createEventsReceived, otherEventsReceived counter go func() { for ev := range watcher.Event { t.Logf("event received: %s", ev) @@ -50,13 +42,9 @@ func TestFsnotifyFakeSymlink(t *testing.T) { } }() - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("Watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testDir) - if os.Symlink("_test/zzz", "_test/zzznew") != nil { + if err := os.Symlink(filepath.Join(testDir, "zzz"), filepath.Join(testDir, "zzznew")); err != nil { t.Fatalf("Failed to create bogus symlink: %s", err) } t.Logf("Created bogus symlink") diff --git a/fsnotify/fsnotify_test.go b/fsnotify/fsnotify_test.go index 269f81653..3f5a6487f 100644 --- a/fsnotify/fsnotify_test.go +++ b/fsnotify/fsnotify_test.go @@ -5,6 +5,7 @@ package fsnotify import ( + "io/ioutil" "os" "os/exec" "path/filepath" @@ -20,23 +21,44 @@ type counter struct { } func (c *counter) increment() { - cas := atomic.CompareAndSwapInt32 - old := atomic.LoadInt32(&c.val) - for swp := cas(&c.val, old, old+1); !swp; swp = cas(&c.val, old, old+1) { - old = atomic.LoadInt32(&c.val) - } + atomic.AddInt32(&c.val, 1) } func (c *counter) value() int32 { return atomic.LoadInt32(&c.val) } -func TestFsnotifyMultipleOperations(t *testing.T) { - // Create an fsnotify watcher instance and initialize it +func (c *counter) reset() { + atomic.StoreInt32(&c.val, 0) +} + +// tempMkdir makes a temporary directory +func tempMkdir(t *testing.T) string { + dir, err := ioutil.TempDir("", "fsnotify") + if err != nil { + t.Fatalf("failed to create test directory: %s", err) + } + return dir +} + +// newWatcher initializes an fsnotify Watcher instance. +func newWatcher(t *testing.T) *Watcher { watcher, err := NewWatcher() if err != nil { t.Fatalf("NewWatcher() failed: %s", err) } + return watcher +} + +// addWatch adds a watch for a directory +func addWatch(t *testing.T, watcher *Watcher, dir string) { + if err := watcher.Watch(dir); err != nil { + t.Fatalf("watcher.Watch(%q) failed: %s", dir, err) + } +} + +func TestFsnotifyMultipleOperations(t *testing.T) { + watcher := newWatcher(t) // Receive errors on the error channel on a separate goroutine go func() { @@ -45,29 +67,19 @@ func TestFsnotifyMultipleOperations(t *testing.T) { } }() - const testDir string = "_test" - const testDirToMoveFiles string = "_test2" - // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) - // Create directory to that's not watched - if err := os.Mkdir(testDirToMoveFiles, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + // Create directory that's not watched + testDirToMoveFiles := tempMkdir(t) defer os.RemoveAll(testDirToMoveFiles) - const testFile string = "_test/TestFsnotifySeq.testfile" - const testFileRenamed string = "_test2/TestFsnotifySeqRename.testfile" + testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") + testFileRenamed := filepath.Join(testDirToMoveFiles, "TestFsnotifySeqRename.testfile") + + addWatch(t, watcher, testDir) - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } // Receive events on the event channel on a separate goroutine eventstream := watcher.Event var createReceived, modifyReceived, deleteReceived, renameReceived counter @@ -99,7 +111,7 @@ func TestFsnotifyMultipleOperations(t *testing.T) { // Create a file // This should add at least one event to the fsnotify event queue var f *os.File - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -112,8 +124,7 @@ func TestFsnotifyMultipleOperations(t *testing.T) { time.Sleep(50 * time.Millisecond) // give system time to sync write change before delete - err = testRename(testFile, testFileRenamed) - if err != nil { + if err := testRename(testFile, testFileRenamed); err != nil { t.Fatalf("rename failed: %s", err) } @@ -165,11 +176,7 @@ func TestFsnotifyMultipleOperations(t *testing.T) { } func TestFsnotifyMultipleCreates(t *testing.T) { - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } + watcher := newWatcher(t) // Receive errors on the error channel on a separate goroutine go func() { @@ -178,21 +185,14 @@ func TestFsnotifyMultipleCreates(t *testing.T) { } }() - const testDir string = "_test" - // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) - const testFile string = "_test/TestFsnotifySeq.testfile" + testFile := filepath.Join(testDir, "TestFsnotifySeq.testfile") + + addWatch(t, watcher, testDir) - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } // Receive events on the event channel on a separate goroutine eventstream := watcher.Event var createReceived, modifyReceived, deleteReceived counter @@ -221,7 +221,7 @@ func TestFsnotifyMultipleCreates(t *testing.T) { // Create a file // This should add at least one event to the fsnotify event queue var f *os.File - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -302,26 +302,18 @@ func TestFsnotifyMultipleCreates(t *testing.T) { } func TestFsnotifyDirOnly(t *testing.T) { - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } - - const testDir string = "_test" + watcher := newWatcher(t) // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) // Create a file before watching directory // This should NOT add any events to the fsnotify event queue - const testFileAlreadyExists string = "_test/TestFsnotifyEventsExisting.testfile" + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") { var f *os.File - f, err = os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -329,11 +321,7 @@ func TestFsnotifyDirOnly(t *testing.T) { f.Close() } - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testDir) // Receive errors on the error channel on a separate goroutine go func() { @@ -342,7 +330,7 @@ func TestFsnotifyDirOnly(t *testing.T) { } }() - const testFile string = "_test/TestFsnotifyDirOnly.testfile" + testFile := filepath.Join(testDir, "TestFsnotifyDirOnly.testfile") // Receive events on the event channel on a separate goroutine eventstream := watcher.Event @@ -372,7 +360,7 @@ func TestFsnotifyDirOnly(t *testing.T) { // Create a file // This should add at least one event to the fsnotify event queue var f *os.File - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -416,25 +404,18 @@ func TestFsnotifyDirOnly(t *testing.T) { } func TestFsnotifyDeleteWatchedDir(t *testing.T) { - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } + watcher := newWatcher(t) defer watcher.Close() - const testDir string = "_test" - // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) // Create a file before watching directory - const testFileAlreadyExists string = "_test/TestFsnotifyEventsExisting.testfile" + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") { var f *os.File - f, err = os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -442,17 +423,10 @@ func TestFsnotifyDeleteWatchedDir(t *testing.T) { f.Close() } - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testDir) // Add a watch for testFile - err = watcher.Watch(testFileAlreadyExists) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testFileAlreadyExists) // Receive errors on the error channel on a separate goroutine go func() { @@ -489,23 +463,16 @@ func TestFsnotifyDeleteWatchedDir(t *testing.T) { } func TestFsnotifySubDir(t *testing.T) { - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } - - const testDir string = "_test" - const testFile1 string = "_test/TestFsnotifyFile1.testfile" - const testSubDir string = "_test/sub" - const testSubDirFile string = "_test/sub/TestFsnotifyFile1.testfile" + watcher := newWatcher(t) // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) + testFile1 := filepath.Join(testDir, "TestFsnotifyFile1.testfile") + testSubDir := filepath.Join(testDir, "sub") + testSubDirFile := filepath.Join(testDir, "sub/TestFsnotifyFile1.testfile") + // Receive errors on the error channel on a separate goroutine go func() { for err := range watcher.Error { @@ -535,11 +502,7 @@ func TestFsnotifySubDir(t *testing.T) { done <- true }() - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testDir) // Create sub-directory if err := os.Mkdir(testSubDir, 0777); err != nil { @@ -548,7 +511,7 @@ func TestFsnotifySubDir(t *testing.T) { // Create a file var f *os.File - f, err = os.OpenFile(testFile1, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFile1, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -594,25 +557,13 @@ func TestFsnotifySubDir(t *testing.T) { } func TestFsnotifyRename(t *testing.T) { - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } - - const testDir string = "_test" + watcher := newWatcher(t) // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testDir) // Receive errors on the error channel on a separate goroutine go func() { @@ -621,8 +572,8 @@ func TestFsnotifyRename(t *testing.T) { } }() - const testFile string = "_test/TestFsnotifyEvents.testfile" - const testFileRenamed string = "_test/TestFsnotifyEvents.testfileRenamed" + testFile := filepath.Join(testDir, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") // Receive events on the event channel on a separate goroutine eventstream := watcher.Event @@ -646,7 +597,7 @@ func TestFsnotifyRename(t *testing.T) { // Create a file // This should add at least one event to the fsnotify event queue var f *os.File - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -657,13 +608,9 @@ func TestFsnotifyRename(t *testing.T) { f.Close() // Add a watch for testFile - err = watcher.Watch(testFile) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testFile) - err = testRename(testFile, testFileRenamed) - if err != nil { + if err := testRename(testFile, testFileRenamed); err != nil { t.Fatalf("rename failed: %s", err) } @@ -688,32 +635,17 @@ func TestFsnotifyRename(t *testing.T) { } func TestFsnotifyRenameToCreate(t *testing.T) { - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } - - const testDir string = "_test" - const testDirFrom string = "_testfrom" + watcher := newWatcher(t) // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) // Create directory to get file - if err := os.Mkdir(testDirFrom, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDirFrom := tempMkdir(t) defer os.RemoveAll(testDirFrom) - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testDir) // Receive errors on the error channel on a separate goroutine go func() { @@ -722,8 +654,8 @@ func TestFsnotifyRenameToCreate(t *testing.T) { } }() - const testFile string = "_testfrom/TestFsnotifyEvents.testfile" - const testFileRenamed string = "_test/TestFsnotifyEvents.testfileRenamed" + testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") // Receive events on the event channel on a separate goroutine eventstream := watcher.Event @@ -747,15 +679,14 @@ func TestFsnotifyRenameToCreate(t *testing.T) { // Create a file // This should add at least one event to the fsnotify event queue var f *os.File - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } f.Sync() f.Close() - err = testRename(testFile, testFileRenamed) - if err != nil { + if err := testRename(testFile, testFileRenamed); err != nil { t.Fatalf("rename failed: %s", err) } @@ -784,44 +715,30 @@ func TestFsnotifyRenameToOverwrite(t *testing.T) { case "plan9", "windows": t.Skipf("skipping test on %q (os.Rename over existing file does not create event).", runtime.GOOS) } - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } - - const testDir string = "_test" - const testDirFrom string = "_testfrom" - const testFile string = "_testfrom/TestFsnotifyEvents.testfile" - const testFileRenamed string = "_test/TestFsnotifyEvents.testfileRenamed" + watcher := newWatcher(t) // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) // Create directory to get file - if err := os.Mkdir(testDirFrom, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDirFrom := tempMkdir(t) defer os.RemoveAll(testDirFrom) + testFile := filepath.Join(testDirFrom, "TestFsnotifyEvents.testfile") + testFileRenamed := filepath.Join(testDir, "TestFsnotifyEvents.testfileRenamed") + // Create a file var fr *os.File - fr, err = os.OpenFile(testFileRenamed, os.O_WRONLY|os.O_CREATE, 0666) + fr, err := os.OpenFile(testFileRenamed, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } fr.Sync() fr.Close() - // Add a watch for testDir - err = watcher.Watch(testDir) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testDir) // Receive errors on the error channel on a separate goroutine go func() { @@ -857,8 +774,7 @@ func TestFsnotifyRenameToOverwrite(t *testing.T) { f.Sync() f.Close() - err = testRename(testFile, testFileRenamed) - if err != nil { + if err := testRename(testFile, testFileRenamed); err != nil { t.Fatalf("rename failed: %s", err) } @@ -882,22 +798,64 @@ func TestFsnotifyRenameToOverwrite(t *testing.T) { os.Remove(testFileRenamed) } +func TestRemovalOfWatch(t *testing.T) { + // Create directory to watch + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + // Create a file before watching directory + testFileAlreadyExists := filepath.Join(testDir, "TestFsnotifyEventsExisting.testfile") + { + var f *os.File + f, err := os.OpenFile(testFileAlreadyExists, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatalf("creating test file failed: %s", err) + } + f.Sync() + f.Close() + } + + watcher := newWatcher(t) + defer watcher.Close() + + addWatch(t, watcher, testDir) + if err := watcher.RemoveWatch(testDir); err != nil { + t.Fatalf("Could not remove the watch: %v\n", err) + } + + go func() { + select { + case ev := <-watcher.Event: + t.Fatalf("We received event: %v\n", ev) + case <-time.After(500 * time.Millisecond): + t.Log("No event received, as expected.") + } + }() + + time.Sleep(200 * time.Millisecond) + // Modify the file outside of the watched dir + f, err := os.Open(testFileAlreadyExists) + if err != nil { + t.Fatalf("Open test file failed: %s", err) + } + f.WriteString("data") + f.Sync() + f.Close() + if err := os.Chmod(testFileAlreadyExists, 0700); err != nil { + t.Fatalf("chmod failed: %s", err) + } + time.Sleep(400 * time.Millisecond) +} + func TestFsnotifyAttrib(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("attributes don't work on Windows.") } - // Create an fsnotify watcher instance and initialize it - watcher, err := NewWatcher() - if err != nil { - t.Fatalf("NewWatcher() failed: %s", err) - } - const testDir string = "_test" + watcher := newWatcher(t) // Create directory to watch - if err := os.Mkdir(testDir, 0777); err != nil { - t.Fatalf("failed to create test directory: %s", err) - } + testDir := tempMkdir(t) defer os.RemoveAll(testDir) // Receive errors on the error channel on a separate goroutine @@ -907,10 +865,14 @@ func TestFsnotifyAttrib(t *testing.T) { } }() - const testFile string = "_test/TestFsnotifyAttrib.testfile" + testFile := filepath.Join(testDir, "TestFsnotifyAttrib.testfile") // Receive events on the event channel on a separate goroutine eventstream := watcher.Event + // The modifyReceived counter counts IsModify events that are not IsAttrib, + // and the attribReceived counts IsAttrib events (which are also IsModify as + // a consequence). + var modifyReceived counter var attribReceived counter done := make(chan bool) go func() { @@ -918,6 +880,9 @@ func TestFsnotifyAttrib(t *testing.T) { // Only count relevant events if event.Name == filepath.Clean(testDir) || event.Name == filepath.Clean(testFile) { if event.IsModify() { + modifyReceived.increment() + } + if event.IsAttrib() { attribReceived.increment() } t.Logf("event received: %s", event) @@ -931,7 +896,7 @@ func TestFsnotifyAttrib(t *testing.T) { // Create a file // This should add at least one event to the fsnotify event queue var f *os.File - f, err = os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) + f, err := os.OpenFile(testFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Fatalf("creating test file failed: %s", err) } @@ -942,22 +907,61 @@ func TestFsnotifyAttrib(t *testing.T) { f.Close() // Add a watch for testFile - err = watcher.Watch(testFile) - if err != nil { - t.Fatalf("watcher.Watch() failed: %s", err) - } + addWatch(t, watcher, testFile) - err = os.Chmod(testFile, 0700) - if err != nil { + if err := os.Chmod(testFile, 0700); err != nil { t.Fatalf("chmod failed: %s", err) } // We expect this event to be received almost immediately, but let's wait 500 ms to be sure + // Creating/writing a file changes also the mtime, so IsAttrib should be set to true here time.Sleep(500 * time.Millisecond) + if modifyReceived.value() == 0 { + t.Fatal("fsnotify modify events have not received after 500 ms") + } if attribReceived.value() == 0 { t.Fatal("fsnotify attribute events have not received after 500 ms") } + // Modifying the contents of the file does not set the attrib flag (although eg. the mtime + // might have been modified). + modifyReceived.reset() + attribReceived.reset() + + f, err = os.OpenFile(testFile, os.O_WRONLY, 0) + if err != nil { + t.Fatalf("reopening test file failed: %s", err) + } + + f.WriteString("more data") + f.Sync() + f.Close() + + time.Sleep(500 * time.Millisecond) + + if modifyReceived.value() != 1 { + t.Fatal("didn't receive a modify event after changing test file contents") + } + + if attribReceived.value() != 0 { + t.Fatal("did receive an unexpected attrib event after changing test file contents") + } + + modifyReceived.reset() + attribReceived.reset() + + // Doing a chmod on the file should trigger an event with the "attrib" flag set (the contents + // of the file are not changed though) + if err := os.Chmod(testFile, 0600); err != nil { + t.Fatalf("chmod failed: %s", err) + } + + time.Sleep(500 * time.Millisecond) + + if attribReceived.value() != 1 { + t.Fatal("didn't receive an attribute change after 500ms") + } + // Try closing the fsnotify instance t.Log("calling Close()") watcher.Close() @@ -973,7 +977,7 @@ func TestFsnotifyAttrib(t *testing.T) { } func TestFsnotifyClose(t *testing.T) { - watcher, _ := NewWatcher() + watcher := newWatcher(t) watcher.Close() var done int32 @@ -987,8 +991,10 @@ func TestFsnotifyClose(t *testing.T) { t.Fatal("double Close() test failed: second Close() call didn't return") } - err := watcher.Watch("_test") - if err == nil { + testDir := tempMkdir(t) + defer os.RemoveAll(testDir) + + if err := watcher.Watch(testDir); err == nil { t.Fatal("expected error on Watch() after Close(), got nil") } } diff --git a/fsnotify/fsnotify_windows.go b/fsnotify/fsnotify_windows.go index 824513099..d88ae6340 100644 --- a/fsnotify/fsnotify_windows.go +++ b/fsnotify/fsnotify_windows.go @@ -41,6 +41,11 @@ const ( sys_FS_Q_OVERFLOW = 0x4000 ) +const ( + // TODO(nj): Use syscall.ERROR_MORE_DATA from ztypes_windows in Go 1.3+ + sys_ERROR_MORE_DATA syscall.Errno = 234 +) + // Event is the type of the notification messages // received on the watcher's Event channel. type FileEvent struct { @@ -49,24 +54,29 @@ type FileEvent struct { Name string // File name (optional) } -// IsCreate reports whether the FileEvent was triggerd by a creation +// IsCreate reports whether the FileEvent was triggered by a creation func (e *FileEvent) IsCreate() bool { return (e.mask & sys_FS_CREATE) == sys_FS_CREATE } -// IsDelete reports whether the FileEvent was triggerd by a delete +// IsDelete reports whether the FileEvent was triggered by a delete func (e *FileEvent) IsDelete() bool { return ((e.mask&sys_FS_DELETE) == sys_FS_DELETE || (e.mask&sys_FS_DELETE_SELF) == sys_FS_DELETE_SELF) } -// IsModify reports whether the FileEvent was triggerd by a file modification or attribute change +// IsModify reports whether the FileEvent was triggered by a file modification or attribute change func (e *FileEvent) IsModify() bool { return ((e.mask&sys_FS_MODIFY) == sys_FS_MODIFY || (e.mask&sys_FS_ATTRIB) == sys_FS_ATTRIB) } -// IsRename reports whether the FileEvent was triggerd by a change name +// IsRename reports whether the FileEvent was triggered by a change name func (e *FileEvent) IsRename() bool { return ((e.mask&sys_FS_MOVE) == sys_FS_MOVE || (e.mask&sys_FS_MOVE_SELF) == sys_FS_MOVE_SELF || (e.mask&sys_FS_MOVED_FROM) == sys_FS_MOVED_FROM || (e.mask&sys_FS_MOVED_TO) == sys_FS_MOVED_TO) } +// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata. +func (e *FileEvent) IsAttrib() bool { + return (e.mask & sys_FS_ATTRIB) == sys_FS_ATTRIB +} + const ( opAddWatch = iota opRemoveWatch @@ -406,7 +416,7 @@ func (w *Watcher) readEvents() { select { case ch := <-w.quit: w.mu.Lock() - indexes := make([]indexMap, 0) + var indexes []indexMap for _, index := range w.watches { indexes = append(indexes, index) } @@ -438,6 +448,15 @@ func (w *Watcher) readEvents() { } switch e { + case sys_ERROR_MORE_DATA: + if watch == nil { + w.Error <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") + } else { + // The i/o succeeded but the buffer is full. + // In theory we should be building up a full packet. + // In practice we can get away with just carrying on. + n = uint32(unsafe.Sizeof(watch.buf)) + } case syscall.ERROR_ACCESS_DENIED: // Watched directory was probably removed w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) @@ -512,6 +531,12 @@ func (w *Watcher) readEvents() { break } offset += raw.NextEntryOffset + + // Error! + if offset >= n { + w.Error <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") + break + } } if err := w.startRead(watch); err != nil {