gopls/internal/filewatcher: synthesize events after watching a dir
Previously, file or subdirectory creations could be missed if they
occurred after a directory was created but before the file watcher was
registered for it. This resulted in missed events for all subsequent
operations within that subdirectory.
This change addresses this gap by performing a breadth-first scan of a
new directory immediately after the watch is established. The scan
iterates over the directory's entries, synthesizes `Create` events for
each one.
Subdirectories discovered during the scan are added to the queue to be
processed recursively. This approach guarantees that no file creation
events are missed, though it may result in duplicate events if the
watcher also captured the creation.
The root dir creation event and all synthesized `Create` are added to
the out atomically to make sure the out slice is always holding a
logically correct file events and ready to flush anytime.
For golang/go#74292
diff --git a/gopls/internal/filewatcher/filewatcher.go b/gopls/internal/filewatcher/filewatcher.go
index 1818b77..19d7113 100644
--- a/gopls/internal/filewatcher/filewatcher.go
+++ b/gopls/internal/filewatcher/filewatcher.go
@@ -184,12 +184,46 @@
continue
}
+ synthesized := []protocol.FileEvent{} // synthesized create events
+
if isDir {
switch e.Type {
case protocol.Created:
- if err := w.watchDir(event.Name); err != nil {
- errHandler(err)
+ queue := []string{event.Name}
+ for len(queue) > 0 {
+ cur := queue[0]
+ queue = queue[1:]
+
+ if err := w.watchDir(cur); err != nil {
+ errHandler(err)
+ continue
+ }
+
+ entries, err := os.ReadDir(cur)
+ if err != nil {
+ errHandler(err)
+ continue
+ }
+ for _, entry := range entries {
+ if entry.IsDir() && skipDir(entry.Name()) {
+ continue
+ }
+ if !entry.IsDir() && skipFile(entry.Name()) {
+ continue
+ }
+
+ path := filepath.Join(cur, entry.Name())
+ synthesized = append(synthesized, protocol.FileEvent{
+ URI: protocol.URIFromPath(path),
+ Type: protocol.Created,
+ })
+
+ if entry.IsDir() {
+ queue = append(queue, path)
+ }
+ }
}
+
case protocol.Deleted:
// Upon removal, we only need to remove the entries from
// the map. The [fsnotify.Watcher] removes the watch for
@@ -203,6 +237,10 @@
}
}
+ // Events discovered should be appended to the out slice atomically.
+ // This make sure at any point, the out slice is holding a
+ // logically correct file events that can be flushed anytime.
+ w.mu.Lock()
// Some systems emit duplicate change events in close
// succession upon file modification. While the current
// deduplication is naive and only handles immediate duplicates,
@@ -212,10 +250,11 @@
// events means all duplicates, regardless of proximity, should
// be removed. Consider checking the entire buffered slice or
// using a map for this.
- w.mu.Lock()
if len(w.out) == 0 || w.out[len(w.out)-1] != e {
w.out = append(w.out, e)
}
+ // Synthesized creation event does not need to deduplicate.
+ w.out = append(w.out, synthesized...)
w.mu.Unlock()
}
}
@@ -246,6 +285,16 @@
return strings.HasPrefix(dirName, ".") || strings.HasPrefix(dirName, "_") || dirName == "testdata"
}
+// skipFile reports whether the file should be skipped.
+func skipFile(fileName string) bool {
+ switch strings.TrimPrefix(filepath.Ext(fileName), ".") {
+ case "go", "mod", "sum", "work", "s":
+ return false
+ default:
+ return true
+ }
+}
+
// WatchDir walks through the directory and all its subdirectories, adding
// them to the watcher.
func (w *Watcher) WatchDir(path string) error {
@@ -284,16 +333,11 @@
}
// Filter out events for directories and files that are not of interest.
- if isDir {
- if skipDir(filepath.Base(event.Name)) {
- return protocol.FileEvent{}, true
- }
- } else {
- switch strings.TrimPrefix(filepath.Ext(event.Name), ".") {
- case "go", "mod", "sum", "work", "s":
- default:
- return protocol.FileEvent{}, false
- }
+ if isDir && skipDir(filepath.Base(event.Name)) {
+ return protocol.FileEvent{}, true
+ }
+ if !isDir && skipFile(filepath.Base(event.Name)) {
+ return protocol.FileEvent{}, false
}
var t protocol.FileChangeType
diff --git a/gopls/internal/filewatcher/filewatcher_test.go b/gopls/internal/filewatcher/filewatcher_test.go
index d239868..8d768e9 100644
--- a/gopls/internal/filewatcher/filewatcher_test.go
+++ b/gopls/internal/filewatcher/filewatcher_test.go
@@ -11,6 +11,7 @@
"path/filepath"
"runtime"
"slices"
+ "strings"
"testing"
"time"
@@ -31,11 +32,11 @@
testCases := []struct {
name string
goos []string // if not empty, only run in these OS.
- // If set, sends watch errors for this path to an error channel
- // passed to the 'changes' func.
- watchErrorPath string
+ // If set, sends watch errors for these path to error channels passed
+ // to the 'changes' func. After successful watch, channel will be closed.
+ watchErrorPath []string
initWorkspace string
- changes func(root string, errs chan error) error
+ changes func(root string, errs map[string]chan error) error
expectedEvents []protocol.FileEvent
}{
{
@@ -45,7 +46,7 @@
-- foo.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.WriteFile(filepath.Join(root, "bar.go"), []byte("package main"), 0644)
},
expectedEvents: []protocol.FileEvent{
@@ -59,7 +60,7 @@
-- foo.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.WriteFile(filepath.Join(root, "bar.go"), []byte("package main"), 0644)
},
expectedEvents: []protocol.FileEvent{
@@ -73,7 +74,7 @@
-- foo.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.WriteFile(filepath.Join(root, "foo.go"), []byte("package main // modified"), 0644)
},
expectedEvents: []protocol.FileEvent{
@@ -88,7 +89,7 @@
-- bar.go --
package bar
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.Remove(filepath.Join(root, "foo.go"))
},
expectedEvents: []protocol.FileEvent{
@@ -102,7 +103,7 @@
-- foo.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.Rename(filepath.Join(root, "foo.go"), filepath.Join(root, "bar.go"))
},
expectedEvents: []protocol.FileEvent{
@@ -117,7 +118,7 @@
-- foo.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.Rename(filepath.Join(root, "foo.go"), filepath.Join(root, "bar.go"))
},
expectedEvents: []protocol.FileEvent{
@@ -131,7 +132,7 @@
-- foo.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.Mkdir(filepath.Join(root, "bar"), 0755)
},
expectedEvents: []protocol.FileEvent{
@@ -144,7 +145,7 @@
-- foo/bar.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.RemoveAll(filepath.Join(root, "foo"))
},
expectedEvents: []protocol.FileEvent{
@@ -166,12 +167,13 @@
-- foo/bar.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.Rename(filepath.Join(root, "foo"), filepath.Join(root, "baz"))
},
expectedEvents: []protocol.FileEvent{
{URI: "foo", Type: protocol.Deleted},
{URI: "baz", Type: protocol.Created},
+ {URI: "baz/bar.go", Type: protocol.Created},
},
},
{
@@ -181,32 +183,50 @@
-- foo/bar.go --
package foo
`,
- changes: func(root string, errs chan error) error {
+ changes: func(root string, errs map[string]chan error) error {
return os.Rename(filepath.Join(root, "foo"), filepath.Join(root, "baz"))
},
expectedEvents: []protocol.FileEvent{
{URI: "baz", Type: protocol.Created},
+ {URI: "baz/bar.go", Type: protocol.Created},
{URI: "foo", Type: protocol.Deleted},
},
},
+ // TOOD(hxjiang): test for symlink to a dir.
{
name: "broken symlink in darwin",
goos: []string{"darwin"},
- watchErrorPath: "foo",
- changes: func(root string, errs chan error) error {
+ watchErrorPath: []string{"foo", "foo/b"},
+ changes: func(root string, errs map[string]chan error) error {
// Prepare a dir with with broken symbolic link.
- // foo <- 1st
- // └── from.go -> root/to.go <- 1st
- tmp := filepath.Join(t.TempDir(), "foo")
- if err := os.Mkdir(tmp, 0755); err != nil {
- return err
- }
- from := filepath.Join(tmp, "from.go")
+ // foo <- 1st
+ // ├── from.go -> root/to.go <- 1st
+ // ├── a.go <- 1st
+ // └── b <- 1st
+ // └── b.go. <- 1st
to := filepath.Join(root, "to.go")
+
+ archive := txtar.Parse([]byte(`
+-- a.go --
+package a
+-- b/b.go --
+package b
+`))
+ tmp := filepath.Join(t.TempDir(), "foo")
+ for _, f := range archive.Files {
+ path := filepath.Join(tmp, f.Name)
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return err
+ }
+ if err := os.WriteFile(path, f.Data, 0644); err != nil {
+ return err
+ }
+ }
+
// Create the symbolic link to a non-existing file. This would
- // cause the watch registration to fail.
- if err := os.Symlink(to, from); err != nil {
+ // cause the watch registration for dir "foo" to fail.
+ if err := os.Symlink(to, filepath.Join(tmp, "from.go")); err != nil {
return err
}
@@ -218,54 +238,80 @@
}
// root
- // ├── foo <- 2nd (Move)
- // │ ├── from.go -> ../to.go <- 2nd (Move)
- // │ └── foo.go <- 4th (Create)
- // └── to.go <- 3rd (Create)
+ // ├── foo <- 2nd (Move)
+ // │ ├── from.go -> ../../to.go <- 2nd (Move)
+ // │ ├── a.go <- 2nd (Move)
+ // │ ├── new.go <- 4th (Create)
+ // │ └── b <- 2nd (Move)
+ // │ ├── b.go <- 2nd (Move)
+ // │ └── new.go <- 4th (Create)
+ // └── to.go <- 3rd (Create)
- // Should be able to capture an error from [fsnotify.Watcher.Add].
- err := <-errs
+ // Should be able to capture watch error while trying to watch
+ // dir "foo".
+ err := <-errs["foo"]
if err == nil {
- return fmt.Errorf("did not capture watch registration failure")
+ return fmt.Errorf("did not capture watch registration failure for dir foo")
}
// The file watcher should retry watch registration and
- // eventually succeed after the file got created.
- if err := os.WriteFile(to, []byte("package main"), 0644); err != nil {
- return err
- }
-
- timer := time.NewTimer(30 * time.Second)
- for {
- var (
- err error
- ok bool
- )
- select {
- case err, ok = <-errs:
- if !ok {
- return fmt.Errorf("can not register watch for foo")
- }
- case <-timer.C:
- return fmt.Errorf("can not register watch for foo after 30 seconds")
+ // eventually succeed watching for all dir under 'foo' after the
+ // file got created.
+ {
+ if err := os.WriteFile(to, []byte("package main"), 0644); err != nil {
+ return err
}
- if err == nil {
- break // watch registration success
+ timer := time.NewTimer(30 * time.Second)
+ defer timer.Stop()
+
+ var watchingFoo, watchingFooB bool
+ for {
+ if watchingFoo && watchingFooB {
+ break
+ }
+
+ select {
+ case _, ok := <-errs["foo"]:
+ if !ok {
+ watchingFoo = true
+ }
+ case _, ok := <-errs["foo/b"]:
+ if !ok {
+ watchingFooB = true
+ }
+ case <-timer.C:
+ var missing []string
+ if !watchingFoo {
+ missing = append(missing, "'foo'")
+ }
+ if !watchingFooB {
+ missing = append(missing, "'foo/b'")
+ }
+ return fmt.Errorf("timed out after 30s waiting for watches on %s to be established", strings.Join(missing, " and "))
+ }
}
}
// Once the watch registration is done, file events under the
- // dir should be captured.
- return os.WriteFile(filepath.Join(root, "foo", "foo.go"), []byte("package main"), 0644)
+ // dir "foo" and "foo/b" should be captured
+ if err := os.WriteFile(filepath.Join(root, "foo", "new.go"), []byte("package main"), 0644); err != nil {
+ return err
+ }
+ return os.WriteFile(filepath.Join(root, "foo", "b", "new.go"), []byte("package main"), 0644)
},
expectedEvents: []protocol.FileEvent{
+ // "foo" create event from fsnotify and synthesized create event
+ // for all entries under foo.
{URI: "foo", Type: protocol.Created},
- // TODO(hxjiang): enable this after implementing retrospectively
- // generate create events.
- // {URI: "foo/from.go", Type: protocol.Created},
+ {URI: "foo/a.go", Type: protocol.Created},
+ {URI: "foo/b", Type: protocol.Created},
+ {URI: "foo/b/b.go", Type: protocol.Created},
+ // "to.go" creation from fsnotify.
{URI: "to.go", Type: protocol.Created},
- {URI: "foo/foo.go", Type: protocol.Created},
+ // file creation event after watch retry succeeded.
+ {URI: "foo/new.go", Type: protocol.Created},
+ {URI: "foo/b/new.go", Type: protocol.Created},
},
},
}
@@ -278,14 +324,22 @@
root := t.TempDir()
- var errs chan error
- if tt.watchErrorPath != "" {
- errs = make(chan error, 10)
- filewatcher.SetAfterAddHook(func(path string, err error) {
- if path == filepath.Join(root, tt.watchErrorPath) {
- errs <- err
- if err == nil {
- close(errs)
+ var errs map[string]chan error
+ if len(tt.watchErrorPath) != 0 {
+ errs = make(map[string]chan error, len(tt.watchErrorPath))
+ for _, path := range tt.watchErrorPath {
+ errs[path] = make(chan error, 10)
+ }
+ filewatcher.SetAfterAddHook(func(path string, watchErr error) {
+ rel, err := filepath.Rel(root, path)
+ if err != nil {
+ return
+ }
+ if ch, ok := errs[filepath.ToSlash(rel)]; ok {
+ if watchErr == nil {
+ close(ch)
+ } else {
+ ch <- watchErr
}
}
})
@@ -303,38 +357,38 @@
}
}
- matched := 0
foundAll := make(chan struct{})
var gots []protocol.FileEvent
- eventHandler := func(events []protocol.FileEvent) {
+
+ index := 0
+ eventsHandler := func(events []protocol.FileEvent) {
+
gots = append(gots, events...)
+
// This verifies that the list of wanted events is a subsequence of
// the received events. It confirms not only that all wanted events
// are present, but also that their relative order is preserved.
for _, got := range events {
- if matched == len(tt.expectedEvents) {
- break
- }
want := protocol.FileEvent{
- URI: protocol.URIFromPath(filepath.Join(root, string(tt.expectedEvents[matched].URI))),
- Type: tt.expectedEvents[matched].Type,
+ URI: protocol.URIFromPath(filepath.Join(root, string(tt.expectedEvents[index].URI))),
+ Type: tt.expectedEvents[index].Type,
}
if want == got {
- matched++
+ index++
+ }
+ if index == len(tt.expectedEvents) {
+ close(foundAll)
}
}
- if matched == len(tt.expectedEvents) {
- close(foundAll)
- }
+
}
errHandler := func(err error) {
t.Errorf("error from watcher: %v", err)
}
- w, err := filewatcher.New(50*time.Millisecond, nil, eventHandler, errHandler)
+ w, err := filewatcher.New(50*time.Millisecond, nil, eventsHandler, errHandler)
if err != nil {
t.Fatal(err)
}
-
defer func() {
if err := w.Close(); err != nil {
t.Errorf("failed to close the file watcher: %v", err)
@@ -354,9 +408,18 @@
select {
case <-foundAll:
case <-time.After(30 * time.Second):
- if matched < len(tt.expectedEvents) {
- t.Errorf("found %v matching events\nall want: %#v\nall got: %#v", matched, tt.expectedEvents, gots)
+ if index != len(tt.expectedEvents) {
+ var want strings.Builder
+ for _, e := range tt.expectedEvents {
+ want.WriteString(fmt.Sprintf("URI: %s type: %v\n", e.URI, e.Type))
+ }
+ var got strings.Builder
+ for _, e := range gots {
+ got.WriteString(fmt.Sprintf("URI: %s type: %v\n", strings.TrimPrefix(e.URI.Path(), root+"/"), e.Type))
+ }
+ t.Errorf("found %v matching events slice\nmissing sequences:\n%s\nall got:\n%s", index, want.String(), got.String())
}
+
}
})
}
@@ -427,6 +490,9 @@
foundAll := make(chan struct{})
eventsHandler := func(events []protocol.FileEvent) {
+ if len(wants) == 0 { // avoid closing twice
+ return
+ }
for _, e := range events {
delete(wants, e)
}
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
synthesized := []protocol.FileEvent{} // synthesized create eventsvar synthesized []protocol.FileEvent
if len(synthesized) > 1 {
synthesized = synthesized[1:]
}Can you just skip path == event.Name above? Might be easier to reason about.
// If set, sends watch errors for these path to error channels passed
// to the 'changes' func. After successful watch, channel will be closed.The most important thing for comments to explain is 'why'.
What purpose is this machinery serving? What are we trying to verify?
changes func(root string, errs map[string]chan error) error'setup'?
changes: func(root string, errs map[string]chan error) error {As far as I can tell, this is the only test that uses the errs argument, whose type is rather complicated, and whose usage is hard to understand.
It feels to me like we're forcing what's really a separate test into this framework. If we want to verify that the watcher reports an error for a broken symlink, just write a `TestBrokenSymlink` that does the minimum amount of work to verify that an error is reported. I think that test doesn't need all the machinery of this one, and separating this complicated error synchronization out of this test should make it much simpler and more maintainable--it's really hard to reason that the test won't hang when we have a map of channels keyed by file names!
if len(wants) == 0 { // avoid closing twice
return
}? I don't understand this. How does closing twice depend on len(wants)?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
synthesized := []protocol.FileEvent{} // synthesized create eventsHongxiang Jiangvar synthesized []protocol.FileEvent
Done
if len(synthesized) > 1 {
synthesized = synthesized[1:]
}Can you just skip path == event.Name above? Might be easier to reason about.
Done
// If set, sends watch errors for these path to error channels passed
// to the 'changes' func. After successful watch, channel will be closed.The most important thing for comments to explain is 'why'.
What purpose is this machinery serving? What are we trying to verify?
Done. Explanation added to the TestBrokenSymlink test case.
changes func(root string, errs map[string]chan error) errorHongxiang Jiang'setup'?
Done
changes: func(root string, errs map[string]chan error) error {As far as I can tell, this is the only test that uses the errs argument, whose type is rather complicated, and whose usage is hard to understand.
It feels to me like we're forcing what's really a separate test into this framework. If we want to verify that the watcher reports an error for a broken symlink, just write a `TestBrokenSymlink` that does the minimum amount of work to verify that an error is reported. I think that test doesn't need all the machinery of this one, and separating this complicated error synchronization out of this test should make it much simpler and more maintainable--it's really hard to reason that the test won't hang when we have a map of channels keyed by file names!
SG. I have moved that as a separate test.
? I don't understand this. How does closing twice depend on len(wants)?
events handler can be called multiple times. It's possible we receiver more events after we found all the expected events.
We delet entries from the map when we see events we want to see.
The first time we found all the events, the map's length is 0, we close the foundAll. But if later, more events arrived, we need to avoid closing foundAll.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
changes func(root string, errs map[string]chan error) errorHongxiang Jiang'setup'?
Done
Oh! This was a bad suggestion! I thought we were setting it up beforehand, but I see that we're actually making changes to expect events.
Feel free to revert, and to tell me when my suggestions are bad 😊
func TestBrokenSymlink(t *testing.T) {If we're just testing one thing, this test seems awfully large. Can't we just have a single broken symlink? The test for traversal is really implemented above.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Auto-Submit | +1 |
| Commit-Queue | +1 |
changes func(root string, errs map[string]chan error) errorHongxiang Jiang'setup'?
Robert FindleyDone
Oh! This was a bad suggestion! I thought we were setting it up beforehand, but I see that we're actually making changes to expect events.
Feel free to revert, and to tell me when my suggestions are bad 😊
Done
If we're just testing one thing, this test seems awfully large. Can't we just have a single broken symlink? The test for traversal is really implemented above.
SG. I have remove the sub dir foo/a and foo/b. So we only wait for foo's watch error and wait for foo's watch to succeed.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
9 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:
```
The name of the file: gopls/internal/filewatcher/filewatcher_test.go
Insertions: 31, Deletions: 68.
@@ -33,7 +33,7 @@
name string
goos []string // if not empty, only run in these OS.
initWorkspace string
- setup func(root string) error
+ changes func(root string) error
expectedEvents []protocol.FileEvent
}{
{
@@ -43,7 +43,7 @@
-- foo.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.WriteFile(filepath.Join(root, "bar.go"), []byte("package main"), 0644)
},
expectedEvents: []protocol.FileEvent{
@@ -57,7 +57,7 @@
-- foo.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.WriteFile(filepath.Join(root, "bar.go"), []byte("package main"), 0644)
},
expectedEvents: []protocol.FileEvent{
@@ -71,7 +71,7 @@
-- foo.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.WriteFile(filepath.Join(root, "foo.go"), []byte("package main // modified"), 0644)
},
expectedEvents: []protocol.FileEvent{
@@ -86,7 +86,7 @@
-- bar.go --
package bar
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.Remove(filepath.Join(root, "foo.go"))
},
expectedEvents: []protocol.FileEvent{
@@ -100,7 +100,7 @@
-- foo.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.Rename(filepath.Join(root, "foo.go"), filepath.Join(root, "bar.go"))
},
expectedEvents: []protocol.FileEvent{
@@ -115,7 +115,7 @@
-- foo.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.Rename(filepath.Join(root, "foo.go"), filepath.Join(root, "bar.go"))
},
expectedEvents: []protocol.FileEvent{
@@ -129,7 +129,7 @@
-- foo.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.Mkdir(filepath.Join(root, "bar"), 0755)
},
expectedEvents: []protocol.FileEvent{
@@ -142,7 +142,7 @@
-- foo/bar.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.RemoveAll(filepath.Join(root, "foo"))
},
expectedEvents: []protocol.FileEvent{
@@ -164,7 +164,7 @@
-- foo/bar.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.Rename(filepath.Join(root, "foo"), filepath.Join(root, "baz"))
},
expectedEvents: []protocol.FileEvent{
@@ -180,7 +180,7 @@
-- foo/bar.go --
package foo
`,
- setup: func(root string) error {
+ changes: func(root string) error {
return os.Rename(filepath.Join(root, "foo"), filepath.Join(root, "baz"))
},
expectedEvents: []protocol.FileEvent{
@@ -257,8 +257,8 @@
t.Fatal(err)
}
- if tt.setup != nil {
- if err := tt.setup(root); err != nil {
+ if tt.changes != nil {
+ if err := tt.changes(root); err != nil {
t.Fatal(err)
}
}
@@ -293,21 +293,17 @@
// watchErrs is used to capture watch errors during directory monitoring.
// This mechanism allows the test to assert that specific directory watches
// initially fail and subsequently recover upon fixing the broken symlink.
- watchErrs := map[string]chan error{
- "foo": make(chan error, 10),
- "foo/a": make(chan error, 10),
- "foo/b": make(chan error, 10),
- }
+ watchErrs := make(chan error, 10)
filewatcher.SetAfterAddHook(func(path string, watchErr error) {
rel, err := filepath.Rel(root, path)
if err != nil {
return
}
- if ch, ok := watchErrs[filepath.ToSlash(rel)]; ok {
+ if rel == "foo" {
if watchErr == nil {
- close(ch)
+ close(watchErrs)
} else {
- ch <- watchErr
+ watchErrs <- watchErr
}
}
})
@@ -322,16 +318,13 @@
// "foo" create event from fsnotify and synthesized create event
// for all entries under foo.
{URI: "foo", Type: protocol.Created},
- {URI: "foo/a", Type: protocol.Created},
- {URI: "foo/a/a.go", Type: protocol.Created},
- {URI: "foo/b", Type: protocol.Created},
- {URI: "foo/b/b.go", Type: protocol.Created},
+ {URI: "foo/a.go", Type: protocol.Created},
+ {URI: "foo/b.go", Type: protocol.Created},
{URI: "foo/from.go", Type: protocol.Created},
// "to.go" creation from fsnotify.
{URI: "to.go", Type: protocol.Created},
// file creation event after watch retry succeeded.
{URI: "foo/new.go", Type: protocol.Created},
- {URI: "foo/b/new.go", Type: protocol.Created},
}
eventsHandler := func(events []protocol.FileEvent) {
gots = append(gots, events...)
@@ -376,17 +369,15 @@
// Prepare a dir with with broken symbolic link.
// foo <- 1st
// ├── from.go -> root/to.go <- 1st
- // ├── a <- 1st
- // │ └── a.go. <- 1st
- // └── b <- 1st
- // └── b.go. <- 1st
+ // ├── a.go <- 1st
+ // └── b.go <- 1st
to := filepath.Join(root, "to.go")
archive := txtar.Parse([]byte(`
--- a/a.go --
+-- a.go --
package a
--- b/b.go --
+-- b.go --
package b
`))
tmp := filepath.Join(t.TempDir(), "foo")
@@ -415,17 +406,14 @@
// root
// ├── foo <- 2nd (Move)
- // │ ├── a <- 2nd (Move)
- // │ │ └── a.go <- 2nd (Move)
- // │ ├── b <- 2nd (Move)
- // │ │ ├── b.go <- 2nd (Move)
- // │ │ └── new.go <- 4th (Create)
+ // │ ├── a.go <- 2nd (Move)
+ // │ ├── b.go <- 2nd (Move)
// │ ├── from.go -> ../../to.go <- 2nd (Move)
// │ └── new.go <- 4th (Create)
// └── to.go <- 3rd (Create)
// Should be able to capture watch error while trying to watch dir "foo".
- if err := <-watchErrs["foo"]; err == nil {
+ if err := <-watchErrs; err == nil {
t.Errorf("did not capture watch registration failure for dir foo")
}
@@ -439,49 +427,24 @@
timer := time.NewTimer(30 * time.Second)
defer timer.Stop()
- var watchingFoo, watchingFooA, watchingFooB bool
+ outer:
for {
- if watchingFoo && watchingFooA && watchingFooB {
- break
- }
-
select {
- case _, ok := <-watchErrs["foo"]:
+ case _, ok := <-watchErrs:
if !ok {
- watchingFoo = true
- }
- case _, ok := <-watchErrs["foo/a"]:
- if !ok {
- watchingFooA = true
- }
- case _, ok := <-watchErrs["foo/b"]:
- if !ok {
- watchingFooB = true
+ break outer
}
case <-timer.C:
- var missing []string
- if !watchingFoo {
- missing = append(missing, "'foo'")
- }
- if !watchingFooA {
- missing = append(missing, "'foo/a'")
- }
- if !watchingFooB {
- missing = append(missing, "'foo/b'")
- }
- t.Errorf("timed out after 30s waiting for watches on %s to be established", strings.Join(missing, " , "))
+ t.Errorf("timed out after 30s waiting for watches on foo to be established")
}
}
}
// Once the watch registration is done, file events under the
- // dir "foo" and "foo/b" should be captured
+ // dir "foo" should be captured
if err := os.WriteFile(filepath.Join(root, "foo", "new.go"), []byte("package main"), 0644); err != nil {
t.Fatalf("fail to write file %v", err)
}
- if err := os.WriteFile(filepath.Join(root, "foo", "b", "new.go"), []byte("package main"), 0644); err != nil {
- t.Fatalf("fail to write file %v", err)
- }
}
select {
```
gopls/internal/filewatcher: synthesize events after watching a dir
Previously, file or subdirectory creations could be missed if they
occurred after a directory was created but before the file watcher was
registered for it. This resulted in missed events for all subsequent
operations within that subdirectory.
This change addresses this gap by performing a breadth-first scan of a
new directory immediately after the watch is established. The scan
iterates over the directory's entries, synthesizes `Create` events for
each one.
The BFS(pre-order) make sure the synthesized events follow logical
order: the parent dir creation events always preceds those of their
children.
The root dir creation event and all synthesized `Create` are added to
the out atomically to make sure the out slice is always holding a
logically correct file events and ready to flush anytime.
Note: file/dir creation events maybe duplicated, one captured by the
scan, one captured by fsnotify but it is promised there is no miss.
For golang/go#74292
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |