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