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