Skip to content

Commit 477e0ab

Browse files
h9jianggopherbot
authored andcommitted
gopls/internal/filewatcher: replace handleEvent with convertEvent
This refactor make the convertEvent a simple function that only handles event conversion. Leaving concurrency to the caller. For golang/go#74292 Change-Id: I072e19731ae323da2a7e5012660532143d8afe57 Reviewed-on: https://go-review.googlesource.com/c/tools/+/692155 Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Hongxiang Jiang <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent bae1876 commit 477e0ab

File tree

1 file changed

+80
-86
lines changed

1 file changed

+80
-86
lines changed

gopls/internal/filewatcher/filewatcher.go

Lines changed: 80 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,48 @@ func (w *Watcher) run(delay time.Duration, handler func([]protocol.FileEvent, er
121121
if !ok {
122122
continue
123123
}
124+
125+
// fsnotify does not guarantee clean filepaths.
126+
event.Name = filepath.Clean(event.Name)
127+
124128
// fsnotify.Event should not be handled concurrently, to preserve their
125129
// original order. For example, if a file is deleted and recreated,
126130
// concurrent handling could process the events in reverse order.
127131
//
128132
// Only reset the timer if a relevant event happened.
129133
// https://github.com/fsnotify/fsnotify?tab=readme-ov-file#why-do-i-get-many-chmod-events
130-
if e := w.handleEvent(event); e != nil {
131-
w.addEvent(*e)
132-
timer.Reset(delay)
134+
e, isDir := w.convertEvent(event)
135+
if e == (protocol.FileEvent{}) {
136+
continue
137+
}
138+
139+
if isDir {
140+
switch e.Type {
141+
case protocol.Created:
142+
// Newly created directories are watched asynchronously to prevent
143+
// a potential deadlock on Windows(see fsnotify/fsnotify#502).
144+
// Errors are reported internally.
145+
if done, release := w.addWatchHandle(event.Name); done != nil {
146+
go func() {
147+
w.errs <- w.watchDir(event.Name, done)
148+
149+
// Only release after the error is sent.
150+
release()
151+
}()
152+
}
153+
case protocol.Deleted:
154+
// Upon removal, we only need to remove the entries from
155+
// the map. The [fsnotify.Watcher] removes the watch for
156+
// us. fsnotify/fsnotify#268
157+
w.removeWatchHandle(event.Name)
158+
default:
159+
// convertEvent enforces that dirs are only Created or Deleted.
160+
panic("impossible")
161+
}
133162
}
163+
164+
w.addEvent(e)
165+
timer.Reset(delay)
134166
}
135167
}
136168
}
@@ -171,108 +203,70 @@ func (w *Watcher) WatchDir(path string) error {
171203
})
172204
}
173205

174-
// handleEvent converts an [fsnotify.Event] to the corresponding [protocol.FileEvent]
175-
// and updates the watcher state, returning nil if the event is not relevant.
176-
//
177-
// To avoid blocking, any required watches for new subdirectories are registered
178-
// asynchronously in a separate goroutine.
179-
func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
180-
// fsnotify does not guarantee clean filepaths.
181-
path := filepath.Clean(event.Name)
182-
183-
var isDir bool
184-
if info, err := os.Stat(path); err == nil {
206+
// convertEvent translates an [fsnotify.Event] into a [protocol.FileEvent].
207+
// It returns the translated event and a boolean indicating if the path was a
208+
// directory. For directories, the event Type is either Created or Deleted.
209+
// It returns empty event for events that should be ignored.
210+
func (w *Watcher) convertEvent(event fsnotify.Event) (_ protocol.FileEvent, isDir bool) {
211+
// Determine if the event is for a directory.
212+
if info, err := os.Stat(event.Name); err == nil {
185213
isDir = info.IsDir()
186214
} else if os.IsNotExist(err) {
187-
// Upon deletion, the file/dir has been removed. fsnotify
188-
// does not provide information regarding the deleted item.
215+
// Upon deletion, the file/dir has been removed. fsnotify does not
216+
// provide information regarding the deleted item.
189217
// Use watchHandles to determine if the deleted item was a directory.
190-
isDir = w.isWatchedDir(path)
218+
isDir = w.isWatchedDir(event.Name)
191219
} else {
192220
// If statting failed, something is wrong with the file system.
193221
// Log and move on.
194222
if w.logger != nil {
195-
w.logger.Error("failed to stat path, skipping event as its type (file/dir) is unknown", "path", path, "err", err)
223+
w.logger.Error("failed to stat path, skipping event as its type (file/dir) is unknown", "path", event.Name, "err", err)
196224
}
197-
return nil
225+
return protocol.FileEvent{}, false
198226
}
199227

228+
// Filter out events for directories and files that are not of interest.
200229
if isDir {
201-
if skipDir(filepath.Base(path)) {
202-
return nil
203-
}
204-
205-
switch {
206-
case event.Op.Has(fsnotify.Rename):
207-
// A rename is treated as a deletion of the old path because the
208-
// fsnotify RENAME event doesn't include the new path. A separate
209-
// CREATE event will be sent for the new path if the destination
210-
// directory is watched.
211-
fallthrough
212-
case event.Op.Has(fsnotify.Remove):
213-
// Upon removal, we only need to remove the entries from the map.
214-
// The [fsnotify.Watcher] removes the watch for us.
215-
// fsnotify/fsnotify#268
216-
w.removeWatchHandle(path)
217-
218-
// TODO(hxjiang): Directory removal events from some LSP clients may
219-
// not include corresponding removal events for child files and
220-
// subdirectories. Should we do some filtering when adding the dir
221-
// deletion event to the events slice.
222-
return &protocol.FileEvent{
223-
URI: protocol.URIFromPath(path),
224-
Type: protocol.Deleted,
225-
}
226-
case event.Op.Has(fsnotify.Create):
227-
// This watch is added asynchronously to prevent a potential
228-
// deadlock on Windows. See fsnotify/fsnotify#502.
229-
// Error encountered will be sent to internal error channel.
230-
if done, release := w.addWatchHandle(path); done != nil {
231-
go func() {
232-
w.errs <- w.watchDir(path, done)
233-
234-
// Only release after the error is sent.
235-
release()
236-
}()
237-
}
238-
239-
return &protocol.FileEvent{
240-
URI: protocol.URIFromPath(path),
241-
Type: protocol.Created,
242-
}
243-
default:
244-
return nil
230+
if skipDir(filepath.Base(event.Name)) {
231+
return protocol.FileEvent{}, true
245232
}
246233
} else {
247-
// Only watch files of interest.
248-
switch strings.TrimPrefix(filepath.Ext(path), ".") {
234+
switch strings.TrimPrefix(filepath.Ext(event.Name), ".") {
249235
case "go", "mod", "sum", "work", "s":
250236
default:
251-
return nil
237+
return protocol.FileEvent{}, false
252238
}
239+
}
253240

254-
var t protocol.FileChangeType
255-
switch {
256-
case event.Op.Has(fsnotify.Rename):
257-
// A rename is treated as a deletion of the old path because the
258-
// fsnotify RENAME event doesn't include the new path. A separate
259-
// CREATE event will be sent for the new path if the destination
260-
// directory is watched.
261-
fallthrough
262-
case event.Op.Has(fsnotify.Remove):
263-
t = protocol.Deleted
264-
case event.Op.Has(fsnotify.Create):
265-
t = protocol.Created
266-
case event.Op.Has(fsnotify.Write):
267-
t = protocol.Changed
268-
default:
269-
return nil // ignore the rest of the events
270-
}
271-
return &protocol.FileEvent{
272-
URI: protocol.URIFromPath(path),
273-
Type: t,
241+
var t protocol.FileChangeType
242+
switch {
243+
case event.Op.Has(fsnotify.Rename):
244+
// A rename is treated as a deletion of the old path because the
245+
// fsnotify RENAME event doesn't include the new path. A separate
246+
// CREATE event will be sent for the new path if the destination
247+
// directory is watched.
248+
fallthrough
249+
case event.Op.Has(fsnotify.Remove):
250+
// TODO(hxjiang): Directory removal events from some LSP clients may
251+
// not include corresponding removal events for child files and
252+
// subdirectories. Should we do some filtering when adding the dir
253+
// deletion event to the events slice.
254+
t = protocol.Deleted
255+
case event.Op.Has(fsnotify.Create):
256+
t = protocol.Created
257+
case event.Op.Has(fsnotify.Write):
258+
if isDir {
259+
return protocol.FileEvent{}, isDir // ignore dir write events
274260
}
261+
t = protocol.Changed
262+
default:
263+
return protocol.FileEvent{}, isDir // ignore the rest of the events
275264
}
265+
266+
return protocol.FileEvent{
267+
URI: protocol.URIFromPath(event.Name),
268+
Type: t,
269+
}, isDir
276270
}
277271

278272
// watchDir registers a watch for a directory, retrying with backoff if it fails.

0 commit comments

Comments
 (0)