Skip to content

Commit c37080d

Browse files
h9jianggopherbot
authored andcommitted
gopls/internal/filewatcher: fix for windows deadlock
New directory watches are registered in a goroutine to prevent a potential deadlock. The underlying fsnotify library can block when adding a watch if its event channel is full (see fsnotify/fsnotify#502). A synchronous registration from our event handler would prevent the channel from being drained, causing the deadlock. Replace field watchedDirs with knownDirs, the point of this field is helping us differentiate between file and directory deletion events, it's better to set/delete it in the main "run()" goroutine instead of a separate go routine. Note: The exported WatchDir method remains synchronous by design to guarantee to the caller that the watch is active when the method returns. For golang/go#74292 Change-Id: I7deffe494ea214b2a545778d965ef1fcc07f5dc6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/687917 Auto-Submit: Hongxiang Jiang <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 4f13866 commit c37080d

File tree

1 file changed

+37
-28
lines changed

1 file changed

+37
-28
lines changed

gopls/internal/filewatcher/filewatcher.go

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ type Watcher struct {
3030

3131
mu sync.Mutex // guards all fields below
3232

33-
// watchedDirs keeps track of which directories are being watched by the
34-
// watcher, explicitly added via [fsnotify.Watcher.Add].
35-
watchedDirs map[string]bool
33+
// knownDirs tracks all known directories to help distinguish between file
34+
// and directory deletion events.
35+
knownDirs map[string]bool
3636

3737
// events is the current batch of unsent file events, which will be sent
3838
// when the timer expires.
@@ -51,10 +51,10 @@ func New(delay time.Duration, logger *slog.Logger, handler func([]protocol.FileE
5151
return nil, err
5252
}
5353
w := &Watcher{
54-
logger: logger,
55-
watcher: watcher,
56-
watchedDirs: make(map[string]bool),
57-
stop: make(chan struct{}),
54+
logger: logger,
55+
watcher: watcher,
56+
knownDirs: make(map[string]bool),
57+
stop: make(chan struct{}),
5858
}
5959

6060
w.wg.Add(1)
@@ -134,6 +134,7 @@ func (w *Watcher) WatchDir(path string) error {
134134
if skipDir(d.Name()) {
135135
return filepath.SkipDir
136136
}
137+
w.addKnownDir(path)
137138
if err := w.watchDir(path); err != nil {
138139
// TODO(hxjiang): retry on watch failures.
139140
return filepath.SkipDir
@@ -143,9 +144,11 @@ func (w *Watcher) WatchDir(path string) error {
143144
})
144145
}
145146

146-
// handleEvent converts a single [fsnotify.Event] to the corresponding
147-
// [protocol.FileEvent] and updates the watcher state.
148-
// Returns nil if the input event is not relevant.
147+
// handleEvent converts an [fsnotify.Event] to the corresponding [protocol.FileEvent]
148+
// and updates the watcher state, returning nil if the event is not relevant.
149+
//
150+
// To avoid blocking, any required watches for new subdirectories are registered
151+
// asynchronously in a separate goroutine.
149152
func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
150153
// fsnotify does not guarantee clean filepaths.
151154
path := filepath.Clean(event.Name)
@@ -157,7 +160,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
157160
// Upon deletion, the file/dir has been removed. fsnotify
158161
// does not provide information regarding the deleted item.
159162
// Use the watchedDirs to determine whether it's a dir.
160-
isDir = w.isDir(path)
163+
isDir = w.isKnownDir(path)
161164
} else {
162165
// If statting failed, something is wrong with the file system.
163166
// Log and move on.
@@ -180,7 +183,10 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
180183
// directory is watched.
181184
fallthrough
182185
case event.Op.Has(fsnotify.Remove):
183-
w.unwatchDir(path)
186+
// Upon removal, we only need to remove the entries from the map.
187+
// The [fsnotify.Watcher] remove the watch for us.
188+
// fsnotify/fsnotify#268
189+
w.removeKnownDir(path)
184190

185191
// TODO(hxjiang): Directory removal events from some LSP clients may
186192
// not include corresponding removal events for child files and
@@ -191,8 +197,13 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
191197
Type: protocol.Deleted,
192198
}
193199
case event.Op.Has(fsnotify.Create):
200+
w.addKnownDir(path)
201+
202+
// This watch is added asynchronously to prevent a potential deadlock
203+
// on Windows. The fsnotify library can block when registering a watch
204+
// if its event channel is full (see fsnotify/fsnotify#502).
194205
// TODO(hxjiang): retry on watch failure.
195-
_ = w.watchDir(path)
206+
go w.watchDir(path)
196207

197208
return &protocol.FileEvent{
198209
URI: protocol.URIFromPath(path),
@@ -233,35 +244,32 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
233244
}
234245
}
235246

247+
// watchDir register the watch for the input dir. This function may be blocking
248+
// because of the issue fsnotify/fsnotify#502.
236249
func (w *Watcher) watchDir(path string) error {
237-
w.mu.Lock()
238-
defer w.mu.Unlock()
239-
240250
// Dir with broken symbolic link can not be watched.
241251
// TODO(hxjiang): is it possible the files/dirs are
242252
// created before the watch is successfully registered.
243-
if err := w.watcher.Add(path); err != nil {
244-
return err
245-
}
246-
w.watchedDirs[path] = true
247-
return nil
253+
return w.watcher.Add(path)
248254
}
249255

250-
func (w *Watcher) unwatchDir(path string) {
256+
func (w *Watcher) addKnownDir(path string) {
251257
w.mu.Lock()
252258
defer w.mu.Unlock()
259+
w.knownDirs[path] = true
260+
}
253261

254-
// Upon removal, we only need to remove the entries from the map.
255-
// The [fsnotify.Watcher] remove the watch for us.
256-
// fsnotify/fsnotify#268
257-
delete(w.watchedDirs, path)
262+
func (w *Watcher) removeKnownDir(path string) {
263+
w.mu.Lock()
264+
defer w.mu.Unlock()
265+
delete(w.knownDirs, path)
258266
}
259267

260-
func (w *Watcher) isDir(path string) bool {
268+
func (w *Watcher) isKnownDir(path string) bool {
261269
w.mu.Lock()
262270
defer w.mu.Unlock()
263271

264-
_, isDir := w.watchedDirs[path]
272+
_, isDir := w.knownDirs[path]
265273
return isDir
266274
}
267275

@@ -298,6 +306,7 @@ func (w *Watcher) sendEvents(handler func([]protocol.FileEvent, error)) {
298306
// and returns any final error.
299307
func (w *Watcher) Close() error {
300308
err := w.watcher.Close()
309+
301310
// Wait for the go routine to finish. So all the channels will be closed and
302311
// all go routine will be terminated.
303312
close(w.stop)

0 commit comments

Comments
 (0)