"io/ioutil"
"os"
"os/exec"
+ "path"
"path/filepath"
"runtime"
"sync/atomic"
watcher.Close()
}
+func TestCyclicSymlink(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("symlinks don't work on Windows.")
+ }
+
+ watcher := newWatcher(t)
+
+ testDir := tempMkdir(t)
+ defer os.RemoveAll(testDir)
+
+ link := path.Join(testDir, "link")
+ if err := os.Symlink(".", link); err != nil {
+ t.Fatalf("could not make symlink: %v", err)
+ }
+ addWatch(t, watcher, testDir)
+
+ var createEventsReceived counter
+ go func() {
+ for ev := range watcher.Events {
+ if ev.Op&Create == Create {
+ createEventsReceived.increment()
+ }
+ }
+ }()
+
+ if err := os.Remove(link); err != nil {
+ t.Fatalf("Error removing link: %v", err)
+ }
+
+ // It would be nice to be able to expect a delete event here, but kqueue has
+ // no way for us to get events on symlinks themselves, because opening them
+ // opens an fd to the file to which they point.
+
+ if err := ioutil.WriteFile(link, []byte("foo"), 0700); err != nil {
+ t.Fatalf("could not make symlink: %v", err)
+ }
+
+ // We expect this event to be received almost immediately, but let's wait 500 ms to be sure
+ time.Sleep(500 * time.Millisecond)
+
+ if got := createEventsReceived.value(); got == 0 {
+ t.Errorf("want at least 1 create event got %v", got)
+ }
+
+ watcher.Close()
+}
+
// TestConcurrentRemovalOfWatch tests that concurrent calls to RemoveWatch do not race.
// See https://codereview.appspot.com/103300045/
// go test -test.run=TestConcurrentRemovalOfWatch -test.cpu=1,1,1,1,1 -race
w.mu.Lock()
w.externalWatches[name] = true
w.mu.Unlock()
- return w.addWatch(name, noteAllEvents)
+ _, err := w.addWatch(name, noteAllEvents)
+ return err
}
// Remove stops watching the the named file or directory (non-recursively).
// addWatch adds name to the watched file set.
// The flags are interpreted as described in kevent(2).
-func (w *Watcher) addWatch(name string, flags uint32) error {
+// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
+func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
var isDir bool
// Make ./name and name equivalent
name = filepath.Clean(name)
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
- return errors.New("kevent instance already closed")
+ return "", errors.New("kevent instance already closed")
}
watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags.
if !alreadyWatching {
fi, err := os.Lstat(name)
if err != nil {
- return err
+ return "", err
}
// Don't watch sockets.
if fi.Mode()&os.ModeSocket == os.ModeSocket {
- return nil
+ return "", nil
}
// Don't watch named pipes.
if fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
- return nil
+ return "", nil
}
// Follow Symlinks
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
name, err = filepath.EvalSymlinks(name)
if err != nil {
- return nil
+ return "", nil
+ }
+
+ w.mu.Lock()
+ _, alreadyWatching = w.watches[name]
+ w.mu.Unlock()
+
+ if alreadyWatching {
+ return name, nil
}
fi, err = os.Lstat(name)
if err != nil {
- return nil
+ return "", nil
}
}
watchfd, err = syscall.Open(name, openMode, 0700)
if watchfd == -1 {
- return err
+ return "", err
}
isDir = fi.IsDir()
const registerAdd = syscall.EV_ADD | syscall.EV_CLEAR | syscall.EV_ENABLE
if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil {
syscall.Close(watchfd)
- return err
+ return "", err
}
if !alreadyWatching {
// Watch the directory if it has not been watched before,
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
w.mu.Lock()
+
watchDir := (flags&syscall.NOTE_WRITE) == syscall.NOTE_WRITE &&
(!alreadyWatching || (w.dirFlags[name]&syscall.NOTE_WRITE) != syscall.NOTE_WRITE)
// Store flags so this watch can be updated later
if watchDir {
if err := w.watchDirectoryFiles(name); err != nil {
- return err
+ return "", err
}
}
}
- return nil
+ return name, nil
}
// readEvents reads from kqueue and converts the received kevents into
for _, fileInfo := range files {
filePath := filepath.Join(dirPath, fileInfo.Name())
- if err := w.internalWatch(filePath, fileInfo); err != nil {
+ filePath, err = w.internalWatch(filePath, fileInfo)
+ if err != nil {
return err
}
}
// like watchDirectoryFiles (but without doing another ReadDir)
- if err := w.internalWatch(filePath, fileInfo); err != nil {
+ filePath, err = w.internalWatch(filePath, fileInfo)
+ if err != nil {
return
}
}
}
-func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) error {
+func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) {
if fileInfo.IsDir() {
// mimic Linux providing delete events for subdirectories
// but preserve the flags used if currently watching subdirectory