github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/watcher/filenotify/poller.go (about) 1 // Package filenotify is adapted from https://github.com/moby/moby/tree/master/pkg/filenotify, Apache-2.0 License. 2 // Hopefully this can be replaced with an external package sometime in the future, see https://github.com/fsnotify/fsnotify/issues/9 3 package filenotify 4 5 import ( 6 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 "sync" 11 "time" 12 13 "github.com/fsnotify/fsnotify" 14 "github.com/gohugoio/hugo/common/herrors" 15 ) 16 17 var ( 18 // errPollerClosed is returned when the poller is closed 19 errPollerClosed = errors.New("poller is closed") 20 // errNoSuchWatch is returned when trying to remove a watch that doesn't exist 21 errNoSuchWatch = errors.New("watch does not exist") 22 ) 23 24 // filePoller is used to poll files for changes, especially in cases where fsnotify 25 // can't be run (e.g. when inotify handles are exhausted) 26 // filePoller satisfies the FileWatcher interface 27 type filePoller struct { 28 // the duration between polls. 29 interval time.Duration 30 // watches is the list of files currently being polled, close the associated channel to stop the watch 31 watches map[string]struct{} 32 // Will be closed when done. 33 done chan struct{} 34 // events is the channel to listen to for watch events 35 events chan fsnotify.Event 36 // errors is the channel to listen to for watch errors 37 errors chan error 38 // mu locks the poller for modification 39 mu sync.Mutex 40 // closed is used to specify when the poller has already closed 41 closed bool 42 } 43 44 // Add adds a filename to the list of watches 45 // once added the file is polled for changes in a separate goroutine 46 func (w *filePoller) Add(name string) error { 47 w.mu.Lock() 48 defer w.mu.Unlock() 49 50 if w.closed { 51 return errPollerClosed 52 } 53 54 item, err := newItemToWatch(name) 55 if err != nil { 56 return err 57 } 58 if item.left.FileInfo == nil { 59 return os.ErrNotExist 60 } 61 62 if w.watches == nil { 63 w.watches = make(map[string]struct{}) 64 } 65 if _, exists := w.watches[name]; exists { 66 return fmt.Errorf("watch exists") 67 } 68 w.watches[name] = struct{}{} 69 70 go w.watch(item) 71 return nil 72 } 73 74 // Remove stops and removes watch with the specified name 75 func (w *filePoller) Remove(name string) error { 76 w.mu.Lock() 77 defer w.mu.Unlock() 78 return w.remove(name) 79 } 80 81 func (w *filePoller) remove(name string) error { 82 if w.closed { 83 return errPollerClosed 84 } 85 86 _, exists := w.watches[name] 87 if !exists { 88 return errNoSuchWatch 89 } 90 delete(w.watches, name) 91 return nil 92 } 93 94 // Events returns the event channel 95 // This is used for notifications on events about watched files 96 func (w *filePoller) Events() <-chan fsnotify.Event { 97 return w.events 98 } 99 100 // Errors returns the errors channel 101 // This is used for notifications about errors on watched files 102 func (w *filePoller) Errors() <-chan error { 103 return w.errors 104 } 105 106 // Close closes the poller 107 // All watches are stopped, removed, and the poller cannot be added to 108 func (w *filePoller) Close() error { 109 w.mu.Lock() 110 defer w.mu.Unlock() 111 112 if w.closed { 113 return nil 114 } 115 w.closed = true 116 close(w.done) 117 for name := range w.watches { 118 w.remove(name) 119 } 120 121 return nil 122 } 123 124 // sendEvent publishes the specified event to the events channel 125 func (w *filePoller) sendEvent(e fsnotify.Event) error { 126 select { 127 case w.events <- e: 128 case <-w.done: 129 return fmt.Errorf("closed") 130 } 131 return nil 132 } 133 134 // sendErr publishes the specified error to the errors channel 135 func (w *filePoller) sendErr(e error) error { 136 select { 137 case w.errors <- e: 138 case <-w.done: 139 return fmt.Errorf("closed") 140 } 141 return nil 142 } 143 144 // watch watches item for changes until done is closed. 145 func (w *filePoller) watch(item *itemToWatch) { 146 ticker := time.NewTicker(w.interval) 147 defer ticker.Stop() 148 149 for { 150 select { 151 case <-ticker.C: 152 case <-w.done: 153 return 154 } 155 156 evs, err := item.checkForChanges() 157 if err != nil { 158 if err := w.sendErr(err); err != nil { 159 return 160 } 161 } 162 163 item.left, item.right = item.right, item.left 164 165 for _, ev := range evs { 166 if err := w.sendEvent(ev); err != nil { 167 return 168 } 169 } 170 171 } 172 } 173 174 // recording records the state of a file or a dir. 175 type recording struct { 176 os.FileInfo 177 178 // Set if FileInfo is a dir. 179 entries map[string]os.FileInfo 180 } 181 182 func (r *recording) clear() { 183 r.FileInfo = nil 184 if r.entries != nil { 185 for k := range r.entries { 186 delete(r.entries, k) 187 } 188 } 189 } 190 191 func (r *recording) record(filename string) error { 192 r.clear() 193 194 fi, err := os.Stat(filename) 195 if err != nil && !herrors.IsNotExist(err) { 196 return err 197 } 198 199 if fi == nil { 200 return nil 201 } 202 203 r.FileInfo = fi 204 205 // If fi is a dir, we watch the files inside that directory (not recursively). 206 // This matches the behaviour of fsnotity. 207 if fi.IsDir() { 208 f, err := os.Open(filename) 209 if err != nil { 210 if herrors.IsNotExist(err) { 211 return nil 212 } 213 return err 214 } 215 defer f.Close() 216 217 fis, err := f.Readdir(-1) 218 if err != nil { 219 if herrors.IsNotExist(err) { 220 return nil 221 } 222 return err 223 } 224 225 for _, fi := range fis { 226 r.entries[fi.Name()] = fi 227 } 228 } 229 230 return nil 231 } 232 233 // itemToWatch may be a file or a dir. 234 type itemToWatch struct { 235 // Full path to the filename. 236 filename string 237 238 // Snapshots of the stat state of this file or dir. 239 left *recording 240 right *recording 241 } 242 243 func newItemToWatch(filename string) (*itemToWatch, error) { 244 r := &recording{ 245 entries: make(map[string]os.FileInfo), 246 } 247 err := r.record(filename) 248 if err != nil { 249 return nil, err 250 } 251 252 return &itemToWatch{filename: filename, left: r}, nil 253 254 } 255 256 func (item *itemToWatch) checkForChanges() ([]fsnotify.Event, error) { 257 if item.right == nil { 258 item.right = &recording{ 259 entries: make(map[string]os.FileInfo), 260 } 261 } 262 263 err := item.right.record(item.filename) 264 if err != nil && !herrors.IsNotExist(err) { 265 return nil, err 266 } 267 268 dirOp := checkChange(item.left.FileInfo, item.right.FileInfo) 269 270 if dirOp != 0 { 271 evs := []fsnotify.Event{fsnotify.Event{Op: dirOp, Name: item.filename}} 272 return evs, nil 273 } 274 275 if item.left.FileInfo == nil || !item.left.IsDir() { 276 // Done. 277 return nil, nil 278 } 279 280 leftIsIn := false 281 left, right := item.left.entries, item.right.entries 282 if len(right) > len(left) { 283 left, right = right, left 284 leftIsIn = true 285 } 286 287 var evs []fsnotify.Event 288 289 for name, fi1 := range left { 290 fi2 := right[name] 291 fil, fir := fi1, fi2 292 if leftIsIn { 293 fil, fir = fir, fil 294 } 295 op := checkChange(fil, fir) 296 if op != 0 { 297 evs = append(evs, fsnotify.Event{Op: op, Name: filepath.Join(item.filename, name)}) 298 } 299 300 } 301 302 return evs, nil 303 304 } 305 306 func checkChange(fi1, fi2 os.FileInfo) fsnotify.Op { 307 if fi1 == nil && fi2 != nil { 308 return fsnotify.Create 309 } 310 if fi1 != nil && fi2 == nil { 311 return fsnotify.Remove 312 } 313 if fi1 == nil && fi2 == nil { 314 return 0 315 } 316 if fi1.IsDir() || fi2.IsDir() { 317 return 0 318 } 319 if fi1.Mode() != fi2.Mode() { 320 return fsnotify.Chmod 321 } 322 if fi1.ModTime() != fi2.ModTime() || fi1.Size() != fi2.Size() { 323 return fsnotify.Write 324 } 325 326 return 0 327 }