@@ -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
}