@@ -8,6 +8,7 @@ package watcher
88
99import (
1010 "context"
11+ "path/filepath"
1112 "sync"
1213
1314 "github.com/fsnotify/fsnotify"
@@ -36,32 +37,91 @@ type Watcher interface {
3637
3738type fsnotifyWatcher struct {
3839 * 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 {}
4146}
4247
48+ // NewFSNotify creates a new file system watcher that watches parent directories
49+ // instead of individual files for more reliable event detection.
4350func NewFSNotify () (Watcher , error ) {
4451 w , err := fsnotify .NewWatcher ()
4552 if err != nil {
4653 return nil , xerrors .Errorf ("create fsnotify watcher: %w" , err )
4754 }
4855 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 ),
5160 }, nil
5261}
5362
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+ }
5784 }
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+
5891 return nil
5992}
6093
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 )
64123 }
124+
65125 return nil
66126}
67127
@@ -73,31 +133,57 @@ func (f *fsnotifyWatcher) Next(ctx context.Context) (event *fsnotify.Event, err
73133 }
74134 }()
75135
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 :
86166 return nil , ErrWatcherClosed
87167 }
88- return nil , xerrors .Errorf ("watcher error: %w" , err )
89- case <- f .closed :
90- return nil , ErrWatcherClosed
91168 }
92169}
93170
94171func (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
103189}
0 commit comments