5
5
package filewatcher
6
6
7
7
import (
8
+ "errors"
8
9
"io/fs"
9
10
"log/slog"
10
11
"os"
@@ -17,22 +18,33 @@ import (
17
18
"golang.org/x/tools/gopls/internal/protocol"
18
19
)
19
20
21
+ // ErrClosed is used when trying to operate on a closed Watcher.
22
+ var ErrClosed = errors .New ("file watcher: watcher already closed" )
23
+
20
24
// Watcher collects events from a [fsnotify.Watcher] and converts them into
21
25
// batched LSP [protocol.FileEvent]s.
22
26
type Watcher struct {
23
27
logger * slog.Logger
24
28
25
29
stop chan struct {} // closed by Close to terminate run loop
26
30
27
- wg sync.WaitGroup // counts number of active run goroutines (max 1)
31
+ // errs is an internal channel for surfacing errors from the file watcher,
32
+ // distinct from the fsnotify watcher's error channel.
33
+ errs chan error
34
+
35
+ // watchers counts the number of active watch registration goroutines,
36
+ // including their error handling.
37
+ watchers sync.WaitGroup
38
+ runners sync.WaitGroup // counts the number of active run goroutines (max 1)
28
39
29
40
watcher * fsnotify.Watcher
30
41
31
42
mu sync.Mutex // guards all fields below
32
43
33
- // knownDirs tracks all known directories to help distinguish between file
34
- // and directory deletion events.
35
- knownDirs map [string ]bool
44
+ // dirCancel maps a directory path to its cancellation channel.
45
+ // A nil map indicates the watcher is closing and prevents new directory
46
+ // watch registrations.
47
+ dirCancel map [string ]chan struct {}
36
48
37
49
// events is the current batch of unsent file events, which will be sent
38
50
// when the timer expires.
@@ -53,11 +65,12 @@ func New(delay time.Duration, logger *slog.Logger, handler func([]protocol.FileE
53
65
w := & Watcher {
54
66
logger : logger ,
55
67
watcher : watcher ,
56
- knownDirs : make (map [string ]bool ),
68
+ dirCancel : make (map [string ]chan struct {}),
69
+ errs : make (chan error ),
57
70
stop : make (chan struct {}),
58
71
}
59
72
60
- w .wg .Add (1 )
73
+ w .runners .Add (1 )
61
74
go w .run (delay , handler )
62
75
63
76
return w , nil
@@ -66,7 +79,7 @@ func New(delay time.Duration, logger *slog.Logger, handler func([]protocol.FileE
66
79
// run is the main event-handling loop for the watcher. It should be run in a
67
80
// separate goroutine.
68
81
func (w * Watcher ) run (delay time.Duration , handler func ([]protocol.FileEvent , error )) {
69
- defer w .wg .Done ()
82
+ defer w .runners .Done ()
70
83
71
84
// timer is used to debounce events.
72
85
timer := time .NewTimer (delay )
@@ -78,13 +91,23 @@ func (w *Watcher) run(delay time.Duration, handler func([]protocol.FileEvent, er
78
91
return
79
92
80
93
case <- timer .C :
81
- w .sendEvents (handler )
94
+ if events := w .drainEvents (); len (events ) > 0 {
95
+ handler (events , nil )
96
+ }
82
97
timer .Reset (delay )
83
98
84
99
case err , ok := <- w .watcher .Errors :
85
100
// When the watcher is closed, its Errors channel is closed, which
86
101
// unblocks this case. We continue to the next loop iteration,
87
- // allowing the <-w.closed case to handle the shutdown.
102
+ // allowing the <-w.stop case to handle the shutdown.
103
+ if ! ok {
104
+ continue
105
+ }
106
+ if err != nil {
107
+ handler (nil , err )
108
+ }
109
+
110
+ case err , ok := <- w .errs :
88
111
if ! ok {
89
112
continue
90
113
}
@@ -96,10 +119,9 @@ func (w *Watcher) run(delay time.Duration, handler func([]protocol.FileEvent, er
96
119
if ! ok {
97
120
continue
98
121
}
99
- // file watcher should not handle the fsnotify.Event concurrently,
100
- // the original order should be preserved. E.g. if a file get
101
- // deleted and recreated, running concurrently may result it in
102
- // reverse order.
122
+ // fsnotify.Event should not be handled concurrently, to preserve their
123
+ // original order. For example, if a file is deleted and recreated,
124
+ // concurrent handling could process the events in reverse order.
103
125
//
104
126
// Only reset the timer if a relevant event happened.
105
127
// https://github.com/fsnotify/fsnotify?tab=readme-ov-file#why-do-i-get-many-chmod-events
@@ -134,10 +156,17 @@ func (w *Watcher) WatchDir(path string) error {
134
156
if skipDir (d .Name ()) {
135
157
return filepath .SkipDir
136
158
}
137
- w .addKnownDir (path )
138
- if err := w .watchDir (path ); err != nil {
139
- // TODO(hxjiang): retry on watch failures.
140
- return filepath .SkipDir
159
+
160
+ done := w .addWatchHandle (path )
161
+ if done == nil { // file watcher closing
162
+ return filepath .SkipAll
163
+ }
164
+
165
+ errChan := make (chan error , 1 )
166
+ w .watchDir (path , done , errChan )
167
+
168
+ if err := <- errChan ; err != nil {
169
+ return err
141
170
}
142
171
}
143
172
return nil
@@ -159,8 +188,8 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
159
188
} else if os .IsNotExist (err ) {
160
189
// Upon deletion, the file/dir has been removed. fsnotify
161
190
// does not provide information regarding the deleted item.
162
- // Use the watchedDirs to determine whether it's a dir .
163
- isDir = w .isKnownDir (path )
191
+ // Use watchHandles to determine if the deleted item was a directory .
192
+ isDir = w .isWatchedDir (path )
164
193
} else {
165
194
// If statting failed, something is wrong with the file system.
166
195
// Log and move on.
@@ -184,26 +213,25 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
184
213
fallthrough
185
214
case event .Op .Has (fsnotify .Remove ):
186
215
// Upon removal, we only need to remove the entries from the map.
187
- // The [fsnotify.Watcher] remove the watch for us.
216
+ // The [fsnotify.Watcher] removes the watch for us.
188
217
// fsnotify/fsnotify#268
189
- w .removeKnownDir (path )
218
+ w .removeWatchHandle (path )
190
219
191
220
// TODO(hxjiang): Directory removal events from some LSP clients may
192
221
// not include corresponding removal events for child files and
193
- // subdirectories. Should we do some filtering when add the dir
222
+ // subdirectories. Should we do some filtering when adding the dir
194
223
// deletion event to the events slice.
195
224
return & protocol.FileEvent {
196
225
URI : protocol .URIFromPath (path ),
197
226
Type : protocol .Deleted ,
198
227
}
199
228
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).
205
- // TODO(hxjiang): retry on watch failure.
206
- go w .watchDir (path )
229
+ // This watch is added asynchronously to prevent a potential
230
+ // deadlock on Windows. See fsnotify/fsnotify#502.
231
+ // Error encountered will be sent to internal error channel.
232
+ if done := w .addWatchHandle (path ); done != nil {
233
+ go w .watchDir (path , done , w .errs )
234
+ }
207
235
208
236
return & protocol.FileEvent {
209
237
URI : protocol .URIFromPath (path ),
@@ -244,10 +272,19 @@ func (w *Watcher) handleEvent(event fsnotify.Event) *protocol.FileEvent {
244
272
}
245
273
}
246
274
247
- // watchDir register the watch for the input dir. This function may be blocking
248
- // because of the issue fsnotify/fsnotify#502.
249
- func (w * Watcher ) watchDir (path string ) error {
250
- // Dir with broken symbolic link can not be watched.
275
+ // watchDir registers a watch for a directory, retrying with backoff if it fails.
276
+ // It can be canceled by calling removeWatchHandle. On success or cancellation,
277
+ // nil is sent to 'errChan'; otherwise, the last error after all retries is sent.
278
+ func (w * Watcher ) watchDir (path string , done chan struct {}, errChan chan error ) {
279
+ if errChan == nil {
280
+ panic ("input error chan is nil" )
281
+ }
282
+
283
+ // On darwin, watching a directory will fail if it contains broken symbolic
284
+ // links. This state can occur temporarily during operations like a git
285
+ // branch switch. To handle this, we retry multiple times with exponential
286
+ // backoff, allowing time for the symbolic link's target to be created.
287
+
251
288
// TODO(hxjiang): Address a race condition where file or directory creations
252
289
// under current directory might be missed between the current directory
253
290
// creation and the establishment of the file watch.
@@ -256,26 +293,89 @@ func (w *Watcher) watchDir(path string) error {
256
293
// 1. Retrospectively check for and trigger creation events for any new
257
294
// files/directories.
258
295
// 2. Recursively add watches for any newly created subdirectories.
259
- return w .watcher .Add (path )
296
+ var (
297
+ delay = 500 * time .Millisecond
298
+ err error
299
+ )
300
+
301
+ // Watchers wait group becomes done only after errChan send.
302
+ w .watchers .Add (1 )
303
+ defer func () {
304
+ errChan <- err
305
+ w .watchers .Done ()
306
+ }()
307
+
308
+ for i := range 5 {
309
+ if i > 0 {
310
+ select {
311
+ case <- time .After (delay ):
312
+ delay *= 2
313
+ case <- done :
314
+ return // cancelled
315
+ }
316
+ }
317
+ // This function may block due to fsnotify/fsnotify#502.
318
+ err := w .watcher .Add (path )
319
+ if afterAddHook != nil {
320
+ afterAddHook (path , err )
321
+ }
322
+ if err == nil {
323
+ return
324
+ }
325
+ }
260
326
}
261
327
262
- func (w * Watcher ) addKnownDir (path string ) {
328
+ var afterAddHook func (path string , err error )
329
+
330
+ // addWatchHandle registers a new directory watch.
331
+ // The returned 'done' channel channel should be used to signal cancellation of
332
+ // a pending watch.
333
+ // It returns nil if the watcher is already closing.
334
+ func (w * Watcher ) addWatchHandle (path string ) chan struct {} {
263
335
w .mu .Lock ()
264
336
defer w .mu .Unlock ()
265
- w .knownDirs [path ] = true
337
+
338
+ if w .dirCancel == nil { // file watcher is closing.
339
+ return nil
340
+ }
341
+
342
+ done := make (chan struct {})
343
+ w .dirCancel [path ] = done
344
+ return done
266
345
}
267
346
268
- func (w * Watcher ) removeKnownDir (path string ) {
347
+ // removeWatchHandle removes the handle for a directory watch and cancels any
348
+ // pending watch attempt for that path.
349
+ func (w * Watcher ) removeWatchHandle (path string ) {
269
350
w .mu .Lock ()
270
351
defer w .mu .Unlock ()
271
- delete (w .knownDirs , path )
352
+
353
+ if done , ok := w .dirCancel [path ]; ok {
354
+ delete (w .dirCancel , path )
355
+ close (done )
356
+ }
357
+ }
358
+
359
+ // close removes all handles and cancels all pending watch attempt for that path
360
+ // and set dirCancel to nil which prevent any future watch attempts.
361
+ func (w * Watcher ) close () {
362
+ w .mu .Lock ()
363
+ dirCancel := w .dirCancel
364
+ w .dirCancel = nil
365
+ w .mu .Unlock ()
366
+
367
+ for _ , ch := range dirCancel {
368
+ close (ch )
369
+ }
272
370
}
273
371
274
- func (w * Watcher ) isKnownDir (path string ) bool {
372
+ // isWatchedDir reports whether the given path has a watch handle, meaning it is
373
+ // a directory the watcher is managing.
374
+ func (w * Watcher ) isWatchedDir (path string ) bool {
275
375
w .mu .Lock ()
276
376
defer w .mu .Unlock ()
277
377
278
- _ , isDir := w .knownDirs [path ]
378
+ _ , isDir := w .dirCancel [path ]
279
379
return isDir
280
380
}
281
381
@@ -297,27 +397,35 @@ func (w *Watcher) addEvent(event protocol.FileEvent) {
297
397
}
298
398
}
299
399
300
- func (w * Watcher ) sendEvents ( handler func ( []protocol.FileEvent , error )) {
400
+ func (w * Watcher ) drainEvents () []protocol.FileEvent {
301
401
w .mu .Lock ()
302
402
events := w .events
303
403
w .events = nil
304
404
w .mu .Unlock ()
305
405
306
- if len (events ) != 0 {
307
- handler (events , nil )
308
- }
406
+ return events
309
407
}
310
408
311
409
// Close shuts down the watcher, waits for the internal goroutine to terminate,
312
410
// and returns any final error.
313
411
func (w * Watcher ) Close () error {
412
+ // Cancel any ongoing watch registration.
413
+ w .close ()
414
+
415
+ // Wait for all watch registration goroutines to finish, including their
416
+ // error handling. This ensures that:
417
+ // - All [Watcher.watchDir] goroutines have exited and sent their errors, so
418
+ // it is safe to close the internal error channel.
419
+ // - There are no ongoing [fsnotify.Watcher.Add] calls, so it is safe to
420
+ // close the fsnotify watcher (see fsnotify/fsnotify#704).
421
+ w .watchers .Wait ()
422
+ close (w .errs )
423
+
314
424
err := w .watcher .Close ()
315
425
316
- // Wait for the go routine to finish. So all the channels will be closed and
317
- // all go routine will be terminated.
426
+ // Wait for the main run loop to terminate.
318
427
close (w .stop )
319
-
320
- w .wg .Wait ()
428
+ w .runners .Wait ()
321
429
322
430
return err
323
431
}
0 commit comments