@@ -8,6 +8,7 @@ package watcher
8
8
9
9
import (
10
10
"context"
11
+ "path/filepath"
11
12
"sync"
12
13
13
14
"github.com/fsnotify/fsnotify"
@@ -36,32 +37,90 @@ type Watcher interface {
36
37
37
38
type fsnotifyWatcher struct {
38
39
* fsnotify.Watcher
39
- closeOnce sync.Once
40
- closed chan struct {}
40
+ closed bool
41
+ done chan struct {}
42
+ mu sync.Mutex // Protects following.
43
+ watchedFiles map [string ]bool // Files being watched (absolute path -> bool).
44
+ watchedDirs map [string ]int // Refcount of directories being watched (absolute path -> count).
41
45
}
42
46
47
+ // NewFSNotify creates a new file system watcher that watches parent directories
48
+ // instead of individual files for more reliable event detection.
43
49
func NewFSNotify () (Watcher , error ) {
44
50
w , err := fsnotify .NewWatcher ()
45
51
if err != nil {
46
52
return nil , xerrors .Errorf ("create fsnotify watcher: %w" , err )
47
53
}
48
54
return & fsnotifyWatcher {
49
- Watcher : w ,
50
- closed : make (chan struct {}),
55
+ Watcher : w ,
56
+ done : make (chan struct {}),
57
+ watchedFiles : make (map [string ]bool ),
58
+ watchedDirs : make (map [string ]int ),
51
59
}, nil
52
60
}
53
61
54
- func (f * fsnotifyWatcher ) Add (path string ) error {
55
- if err := f .Watcher .Add (path ); err != nil {
56
- return xerrors .Errorf ("add path to watcher: %w" , err )
62
+ func (f * fsnotifyWatcher ) Add (file string ) error {
63
+ absPath , err := filepath .Abs (file )
64
+ if err != nil {
65
+ return xerrors .Errorf ("absolute path: %w" , err )
66
+ }
67
+
68
+ dir := filepath .Dir (absPath )
69
+
70
+ f .mu .Lock ()
71
+ defer f .mu .Unlock ()
72
+
73
+ // Already watching this file.
74
+ if f .watchedFiles [absPath ] {
75
+ return nil
57
76
}
77
+
78
+ // Start watching the parent directory if not already watching.
79
+ if f .watchedDirs [dir ] == 0 {
80
+ if err := f .Watcher .Add (dir ); err != nil {
81
+ return xerrors .Errorf ("add directory to watcher: %w" , err )
82
+ }
83
+ }
84
+
85
+ // Increment the reference count for this directory.
86
+ f .watchedDirs [dir ]++
87
+ // Mark this file as watched.
88
+ f .watchedFiles [absPath ] = true
89
+
58
90
return nil
59
91
}
60
92
61
- func (f * fsnotifyWatcher ) Remove (path string ) error {
62
- if err := f .Watcher .Remove (path ); err != nil {
63
- return xerrors .Errorf ("remove path from watcher: %w" , err )
93
+ func (f * fsnotifyWatcher ) Remove (file string ) error {
94
+ absPath , err := filepath .Abs (file )
95
+ if err != nil {
96
+ return xerrors .Errorf ("absolute path: %w" , err )
64
97
}
98
+
99
+ dir := filepath .Dir (absPath )
100
+
101
+ f .mu .Lock ()
102
+ defer f .mu .Unlock ()
103
+
104
+ // Not watching this file.
105
+ if ! f .watchedFiles [absPath ] {
106
+ return nil
107
+ }
108
+
109
+ // Remove the file from our watch list.
110
+ delete (f .watchedFiles , absPath )
111
+
112
+ // Decrement the reference count for this directory.
113
+ f .watchedDirs [dir ]--
114
+
115
+ // If no more files in this directory are being watched, stop
116
+ // watching the directory.
117
+ if f .watchedDirs [dir ] <= 0 {
118
+ if err := f .Watcher .Remove (dir ); err != nil {
119
+ return xerrors .Errorf ("remove directory from watcher: %w" , err )
120
+ }
121
+ delete (f .watchedDirs , dir )
122
+ }
123
+
65
124
return nil
66
125
}
67
126
@@ -73,31 +132,57 @@ func (f *fsnotifyWatcher) Next(ctx context.Context) (event *fsnotify.Event, err
73
132
}
74
133
}()
75
134
76
- select {
77
- case <- ctx .Done ():
78
- return nil , ctx .Err ()
79
- case event , ok := <- f .Events :
80
- if ! ok {
81
- return nil , ErrWatcherClosed
82
- }
83
- return & event , nil
84
- case err , ok := <- f .Errors :
85
- if ! ok {
135
+ for {
136
+ select {
137
+ case <- ctx .Done ():
138
+ return nil , ctx .Err ()
139
+ case evt , ok := <- f .Events :
140
+ if ! ok {
141
+ return nil , ErrWatcherClosed
142
+ }
143
+
144
+ // Get the absolute path to match against our watched files.
145
+ absPath , err := filepath .Abs (evt .Name )
146
+ if err != nil {
147
+ continue
148
+ }
149
+
150
+ f .mu .Lock ()
151
+ isWatched := f .watchedFiles [absPath ]
152
+ f .mu .Unlock ()
153
+ if isWatched {
154
+ return & evt , nil
155
+ }
156
+
157
+ continue // Ignore events for files not being watched.
158
+
159
+ case err , ok := <- f .Errors :
160
+ if ! ok {
161
+ return nil , ErrWatcherClosed
162
+ }
163
+ return nil , xerrors .Errorf ("watcher error: %w" , err )
164
+ case <- f .done :
86
165
return nil , ErrWatcherClosed
87
166
}
88
- return nil , xerrors .Errorf ("watcher error: %w" , err )
89
- case <- f .closed :
90
- return nil , ErrWatcherClosed
91
167
}
92
168
}
93
169
94
170
func (f * fsnotifyWatcher ) Close () (err error ) {
95
- err = ErrWatcherClosed
96
- f .closeOnce .Do (func () {
97
- if err = f .Watcher .Close (); err != nil {
98
- err = xerrors .Errorf ("close watcher: %w" , err )
99
- }
100
- close (f .closed )
101
- })
102
- return err
171
+ f .mu .Lock ()
172
+ f .watchedFiles = nil
173
+ f .watchedDirs = nil
174
+ closed := f .closed
175
+ f .mu .Unlock ()
176
+
177
+ if closed {
178
+ return ErrWatcherClosed
179
+ }
180
+
181
+ close (f .done )
182
+
183
+ if err := f .Watcher .Close (); err != nil {
184
+ return xerrors .Errorf ("close watcher: %w" , err )
185
+ }
186
+
187
+ return nil
103
188
}
0 commit comments