diff --git a/lib/fs/basicfs_watch.go b/lib/fs/basicfs_watch.go index f00e82060..252d217bc 100644 --- a/lib/fs/basicfs_watch.go +++ b/lib/fs/basicfs_watch.go @@ -12,6 +12,7 @@ import ( "context" "errors" "path/filepath" + "runtime" "github.com/syncthing/notify" ) @@ -22,12 +23,7 @@ import ( var backendBuffer = 500 func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, error) { - evalRoot, err := evalSymlinks(f.root) - if err != nil { - return nil, err - } - - absName, err := rooted(name, evalRoot) + watchPath, root, err := f.watchPaths(name) if err != nil { return nil, err } @@ -42,11 +38,11 @@ func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context if ignore.SkipIgnoredDirs() { absShouldIgnore := func(absPath string) bool { - return ignore.ShouldIgnore(f.unrootedChecked(absPath, evalRoot)) + return ignore.ShouldIgnore(f.unrootedChecked(absPath, root)) } - err = notify.WatchWithFilter(filepath.Join(absName, "..."), backendChan, absShouldIgnore, eventMask) + err = notify.WatchWithFilter(watchPath, backendChan, absShouldIgnore, eventMask) } else { - err = notify.Watch(filepath.Join(absName, "..."), backendChan, eventMask) + err = notify.Watch(watchPath, backendChan, eventMask) } if err != nil { notify.Stop(backendChan) @@ -56,11 +52,33 @@ func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context return nil, err } - go f.watchLoop(name, evalRoot, backendChan, outChan, ignore, ctx) + go f.watchLoop(name, root, backendChan, outChan, ignore, ctx) return outChan, nil } +// watchPaths adjust the folder root for use with the notify backend and the +// corresponding absolute path to be passed to notify to watch name. +func (f *BasicFilesystem) watchPaths(name string) (string, string, error) { + root, err := evalSymlinks(f.root) + if err != nil { + return "", "", err + } + + // Remove `\\?\` prefix if the path is just a drive letter as a dirty + // fix for https://github.com/syncthing/syncthing/issues/5578 + if runtime.GOOS == "windows" && filepath.Clean(name) == "." && len(root) <= 7 && len(root) > 4 && root[:4] == `\\?\` { + root = root[4:] + } + + absName, err := rooted(name, root) + if err != nil { + return "", "", err + } + + return filepath.Join(absName, "..."), root, nil +} + func (f *BasicFilesystem) watchLoop(name, evalRoot string, backendChan chan notify.EventInfo, outChan chan<- Event, ignore Matcher, ctx context.Context) { for { // Detect channel overflow diff --git a/lib/fs/basicfs_watch_test.go b/lib/fs/basicfs_watch_test.go index 40bb40190..939098e59 100644 --- a/lib/fs/basicfs_watch_test.go +++ b/lib/fs/basicfs_watch_test.go @@ -149,6 +149,53 @@ func TestWatchRename(t *testing.T) { testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{}) } +// TestWatchWinRoot checks that a watch at a drive letter does not panic due to +// out of root event on every event. +// https://github.com/syncthing/syncthing/issues/5695 +func TestWatchWinRoot(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows specific test") + } + + outChan := make(chan Event) + backendChan := make(chan notify.EventInfo, backendBuffer) + + ctx, cancel := context.WithCancel(context.Background()) + + // testFs is Filesystem, but we need BasicFilesystem here + root := `D:\` + fs := newBasicFilesystem(root) + watch, root, err := fs.watchPaths(".") + if err != nil { + t.Fatal(err) + } + + go func() { + defer func() { + if r := recover(); r != nil { + t.Error(r) + } + cancel() + }() + fs.watchLoop(".", root, backendChan, outChan, fakeMatcher{}, ctx) + }() + + // filepath.Dir as watch has a /... suffix + name := "foo" + backendChan <- fakeEventInfo(filepath.Join(filepath.Dir(watch), name)) + + select { + case <-time.After(10 * time.Second): + cancel() + t.Errorf("Timed out before receiving event") + case ev := <-outChan: + if ev.Name != name { + t.Errorf("Unexpected event %v, expected %v", ev.Name, name) + } + case <-ctx.Done(): + } +} + // TestWatchOutside checks that no changes from outside the folder make it in func TestWatchOutside(t *testing.T) { outChan := make(chan Event) @@ -391,7 +438,7 @@ func testScenario(t *testing.T, name string, testCase func(), expectedEvents, al testCase() select { - case <-time.After(time.Minute): + case <-time.After(10 * time.Second): t.Errorf("Timed out before receiving all expected events") case <-ctx.Done():