diff --git a/internal/watch/file_watcher.go b/internal/watch/file_watcher.go index 0d470569a..b393045b2 100644 --- a/internal/watch/file_watcher.go +++ b/internal/watch/file_watcher.go @@ -18,7 +18,9 @@ package watch import ( "log" + "os" "path" + "path/filepath" "strings" "github.com/fsnotify/fsnotify" @@ -60,15 +62,31 @@ func (f *OSFileWatcher) watch() error { } f.watcher = watcher + realFile, err := filepath.EvalSymlinks(f.file) + if err != nil { + return err + } + dir, file := path.Split(f.file) go func(file string) { for { select { case event := <-watcher.Events: - if (event.Op&fsnotify.Write == fsnotify.Write || - event.Op&fsnotify.Create == fsnotify.Create) && - strings.HasSuffix(event.Name, file) { - f.onEvent() + if event.Op&fsnotify.Create == fsnotify.Create || + event.Op&fsnotify.Write == fsnotify.Write { + if finfo, err := os.Lstat(event.Name); err != nil { + log.Printf("can not lstat file: %v\n", err) + } else if finfo.Mode()&os.ModeSymlink != 0 { + if currentRealFile, err := filepath.EvalSymlinks(f.file); err == nil && + currentRealFile != realFile { + f.onEvent() + realFile = currentRealFile + } + continue + } + if strings.HasSuffix(event.Name, file) { + f.onEvent() + } } case err := <-watcher.Errors: if err != nil { diff --git a/internal/watch/file_watcher_test.go b/internal/watch/file_watcher_test.go index 83a37ea0b..7b39c4e30 100644 --- a/internal/watch/file_watcher_test.go +++ b/internal/watch/file_watcher_test.go @@ -19,6 +19,8 @@ package watch import ( "io/ioutil" "os" + "path" + "path/filepath" "testing" "time" @@ -67,3 +69,73 @@ func TestFileWatcher(t *testing.T) { t.Fatalf("expected an event shortly after writing a file") } } + +func TestFileWatcherWithNestedSymlink(t *testing.T) { + target1, err := ioutil.TempFile("", "t1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer target1.Close() + defer os.Remove(target1.Name()) + dir := path.Dir(target1.Name()) + + innerLink := path.Join(dir, "innerLink") + if err = os.Symlink(target1.Name(), innerLink); err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer os.Remove(innerLink) + mainLink := path.Join(dir, "mainLink") + if err = os.Symlink(innerLink, mainLink); err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer os.Remove(mainLink) + + targetName, err := filepath.EvalSymlinks(mainLink) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if targetName != target1.Name() { + t.Fatalf("expected symlink to point to %v, not %v", target1.Name(), targetName) + } + + count := 0 + events := make(chan bool, 10) + fw, err := NewFileWatcher(mainLink, func() { + count++ + if count != 1 { + t.Fatalf("expected 1 but returned %v", count) + } + if targetName, err = filepath.EvalSymlinks(mainLink); err != nil { + t.Fatalf("unexpected error: %v", err) + } + events <- true + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer fw.Close() + + target2, err := ioutil.TempFile("", "t2") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer target2.Close() + defer os.Remove(target2.Name()) + + if err = os.Remove(innerLink); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err = os.Symlink(target2.Name(), innerLink); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + timeoutChan := prepareTimeout() + select { + case <-events: + case <-timeoutChan: + t.Fatalf("expected an event shortly after creating a file and relinking") + } + if targetName != target2.Name() { + t.Fatalf("expected symlink to point to %v, not %v", target2.Name(), targetName) + } +}