github.com/cilium/cilium@v1.16.2/pkg/fswatcher/fswatcher.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package fswatcher 5 6 import ( 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 12 "github.com/fsnotify/fsnotify" 13 "github.com/sirupsen/logrus" 14 15 "github.com/cilium/cilium/pkg/counter" 16 "github.com/cilium/cilium/pkg/logging" 17 "github.com/cilium/cilium/pkg/logging/logfields" 18 ) 19 20 var log = logging.DefaultLogger.WithField(logfields.LogSubsys, "fswatcher") 21 22 // Event currently wraps fsnotify.Event 23 type Event fsnotify.Event 24 25 // Watcher is a wrapper around fsnotify.Watcher which can track non-existing 26 // files and emit creation events for them. All files which are supposed to be 27 // tracked need to passed to the New constructor. 28 // 1. If the file already exists, the watcher will emit write, chmod, remove 29 // and rename events for the file (same as fsnotify). 30 // 2. If the file does not yet exist, then the Watcher makes sure to watch 31 // the appropriate parent folder instead. Once the file is created, this 32 // watcher will emit a creation event for the tracked file and enter 33 // case 1. 34 // 3. If the file already exists, but is removed, then a remove event is 35 // emitted and we enter case 2. 36 // 37 // Special care has to be taken around symlinks. Support for symlink is 38 // limited, but it supports the following cases in order to support 39 // Kubernetes volume mounts: 40 // 1. If the tracked file is a symlink, then the watcher will emit write, 41 // chmod, remove and rename events for the *target* of the symlink. 42 // 2. If a tracked file is a symlink and the symlink target is removed, 43 // then the remove event is emitted and the watcher tries to re-resolve 44 // the symlink target. If the new target exists, a creation event is 45 // emitted and we enter case 1). If the new target does not exist, an 46 // error is emitted and the path will not be watched anymore. 47 // 48 // Most notably, if a tracked file is a symlink, any update of the symlink 49 // itself does not emit an event. Only if the target of the symlink observes 50 // an event is the symlink re-evaluated. 51 type Watcher struct { 52 watcher *fsnotify.Watcher 53 54 // Internally, we distinguish between 55 watchedPathCount counter.Counter[string] 56 trackedToWatchedPath map[string]string 57 58 // Events is used to signal changes to any of the tracked files. It is 59 // guaranteed that Event.Name will always match one of the file paths 60 // passed in trackedFiles to the constructor. This channel is unbuffered 61 // and must be read by the consumer to avoid deadlocks. 62 Events chan Event 63 // Errors reports any errors which may occur while watching. This channel 64 // is unbuffered and must be read by the consumer to avoid deadlocks. 65 Errors chan error 66 67 // stop channel used to indicate shutdown 68 stop chan struct{} 69 } 70 71 // New creates a new Watcher which watches all trackedFile paths (they do not 72 // need to exist yet). 73 func New(trackedFiles []string) (*Watcher, error) { 74 watcher, err := fsnotify.NewWatcher() 75 if err != nil { 76 return nil, err 77 } 78 79 w := &Watcher{ 80 watcher: watcher, 81 watchedPathCount: counter.Counter[string]{}, 82 trackedToWatchedPath: map[string]string{}, 83 Events: make(chan Event), 84 Errors: make(chan error), 85 stop: make(chan struct{}), 86 } 87 88 // We add all paths in the constructor avoid the need for additional 89 // synchronization, as the loop goroutine below will call updateWatchedPath 90 // concurrently 91 for _, f := range trackedFiles { 92 err := w.updateWatchedPath(f) 93 if err != nil { 94 return nil, err 95 } 96 } 97 98 go w.loop() 99 100 return w, nil 101 } 102 103 func (w *Watcher) Close() { 104 close(w.stop) 105 } 106 107 func (w *Watcher) updateWatchedPath(trackedPath string) error { 108 trackedPath = filepath.Clean(trackedPath) 109 110 // Remove old watchedPath 111 oldWatchedPath, ok := w.trackedToWatchedPath[trackedPath] 112 if ok { 113 w.stopWatching(oldWatchedPath) 114 } 115 116 // Finds and watches the new watchedPath 117 watchedPath, err := w.startWatching(trackedPath) 118 if err != nil { 119 return fmt.Errorf("failed to add fsnotify watcher for %q (parent of %q): %w", 120 watchedPath, trackedPath, err) 121 } 122 123 // Update the mapping 124 w.trackedToWatchedPath[trackedPath] = watchedPath 125 return nil 126 } 127 128 func (w *Watcher) startWatching(path string) (string, error) { 129 // If the path is already watched, we do not want to add it to fsnotify 130 // again, thus the check on the refcount first. 131 // Note: If we already watchedPath has been invalidated recently, 132 // this if statement will be false (because invalidateWatch resets the 133 // count) 134 if w.watchedPathCount[path] > 0 { 135 w.watchedPathCount.Add(path) 136 return path, nil 137 } 138 139 // Adds the file to fsnotify. Important note: If path is a symlink, this 140 // will watch the *target* of the symlink. So any event we will observe, 141 // will be valid for the target, not for the symlink itself. The reported 142 // path in the events however will remain the path of the symlink. 143 err := w.watcher.Add(path) 144 if err != nil { 145 // if the path does not exist, try to watch its parent instead 146 if errors.Is(err, os.ErrNotExist) { 147 parent := filepath.Dir(path) 148 if parent != path { 149 return w.startWatching(parent) 150 } 151 } 152 153 return "", err 154 } 155 156 // Start counting the references for the watched path. 157 // The following is identical to `w.watchedPathCount[path] = 1`, because 158 // w.watchedPathCount[path] was zero when we entered the function 159 w.watchedPathCount.Add(path) 160 return path, nil 161 } 162 163 func (w *Watcher) stopWatching(path string) { 164 // Decrease the refcount for the old watchedPath. If this was the last 165 // use of this watchedPath, we remove it from the underlying fsnotify 166 // watcher. 167 if w.watchedPathCount.Delete(path) { 168 _ = w.watcher.Remove(path) 169 } 170 } 171 172 func (w *Watcher) invalidateWatch(path string) { 173 if w.watchedPathCount[path] > 0 { 174 delete(w.watchedPathCount, path) 175 // The result is ignored because fsnotify removes deleted paths by 176 // itself, in which case it will complain about a non-existing path 177 // being removed. 178 _ = w.watcher.Remove(path) 179 } 180 } 181 182 // hasParent returns true if path is a child or equal to parent 183 func hasParent(path, parent string) bool { 184 path = filepath.Clean(path) 185 parent = filepath.Clean(parent) 186 if path == parent { 187 return true 188 } 189 190 for { 191 pathParent := filepath.Dir(path) 192 if pathParent == parent { 193 return true 194 } 195 196 // reached the root 197 if pathParent == path { 198 return false 199 } 200 201 path = pathParent 202 } 203 } 204 205 // loop filters and processes fsnoity events. It may generate artificial 206 // `Create` events in case observes that files which did not exist before now 207 // exist. This exits after w.Close() is called 208 func (w *Watcher) loop() { 209 for { 210 select { 211 case event := <-w.watcher.Events: 212 scopedLog := log.WithFields(logrus.Fields{ 213 logfields.Path: event.Name, 214 "operation": event.Op, 215 }) 216 scopedLog.Debug("Received fsnotify event") 217 218 eventPath := event.Name 219 removed := event.Has(fsnotify.Remove) 220 renamed := event.Has(fsnotify.Rename) 221 created := event.Has(fsnotify.Create) 222 written := event.Has(fsnotify.Write) 223 224 // If a the eventPath has been removed or renamed, it can no longer 225 // be a valid watchPath. This is needed such that each trackedPath 226 // is updated with a new valid watchPath in the call 227 // to updateWatchedPath below. 228 eventPathInvalidated := removed || renamed 229 if eventPathInvalidated { 230 w.invalidateWatch(eventPath) 231 } 232 233 // We iterate over all tracked files here, checking either if 234 // the event affects the trackedPath (in which case we want to 235 // forward it) and to check if the event affects the watchedPath, 236 // in which case we likely need to update the watchedPath 237 for trackedPath, watchedPath := range w.trackedToWatchedPath { 238 // If the event happened on a tracked path, we can forward 239 // it in all cases 240 if eventPath == trackedPath { 241 w.sendEvent(Event{ 242 Name: trackedPath, 243 Op: event.Op, 244 }) 245 } 246 247 // If the event path has been invalidated (i.e. removed or 248 // renamed), we need to update the watchedPath for this file 249 if eventPathInvalidated && eventPath == watchedPath { 250 // In this case, the watchedPath has been invalidated. There 251 // are multiple cases which are handled by the call to 252 // updateWatchedPath below: 253 // - watchedPath == trackedPath: 254 // - trackedPath is a regular file: 255 // In this case, the tracked file has been deleted or 256 // moved away. This means updateWatchedPath will start 257 // watching a parent folder of trackedPath to pick up 258 // the creation event. 259 // - trackedPath is a symlink: 260 // This means the target of the symlink has been deleted. 261 // If the symlink already points to a new valid target 262 // (this e.g. happens in Kubernetes volume mounts. In, 263 // that case the new target of the symlink will be the 264 // new watchedPath. 265 // - watchedPath was a parent of trackedPath 266 // In this case we will start watching a parent of 267 // the old watchedPath. 268 err := w.updateWatchedPath(trackedPath) 269 if err != nil { 270 w.sendError(err) 271 } 272 273 // If trackedPath is a symlink, it can happen that the old 274 // symlink target was deleted, but symlink itself has been 275 // redirected to a new target. We can detect this, if 276 // after the call to `updateWatchedPath` above, the 277 // tracked and watched path are identical. In such a 278 // case, we emit a create event for the symlink. 279 newWatchedPath := w.trackedToWatchedPath[trackedPath] 280 if newWatchedPath == trackedPath { 281 w.sendEvent(Event{ 282 Name: trackedPath, 283 Op: fsnotify.Create, 284 }) 285 } 286 } 287 288 if created || written { 289 // If a new eventPath been created or written to, we need 290 // to check if the new eventPath should be watched. There 291 // are two conditions (both have to be true): 292 // - eventPath is a parent of trackedPath. If it is not, 293 // then it is unrelated to the file we are trying to track. 294 parentOfTrackedPath := hasParent(trackedPath, eventPath) 295 // - eventPath is a child of the current watchedPath. In 296 // other words, eventPath is a better candidate to watch 297 // than our current watchedPath. 298 childOfWatchedPath := hasParent(eventPath, watchedPath) 299 // Example: 300 // watchedPath: /tmp (we are watching this) 301 // eventPath: /tmp/foo (this was just created, it should become the new watchedPath) 302 // trackedPath: /tmp/foo/bar (we want emit an event if is created) 303 if childOfWatchedPath && parentOfTrackedPath { 304 // The event happened on a child of the watchedPath 305 // and a parent of the trackedPath. This means that 306 // we have found a better watched path. 307 err := w.updateWatchedPath(trackedPath) 308 if err != nil { 309 w.sendError(err) 310 } 311 312 // This checks if the new watchedPath after the call 313 // to `updateWatchedPath` is now equal to the trackedPath. 314 // This implies that the creation of a parent of the 315 // trackedPath has also led to the trackedPath itself 316 // existing now. This can happen e.g. if the parent was 317 // a symlink. 318 newWatchedPath := w.trackedToWatchedPath[trackedPath] 319 if newWatchedPath == trackedPath { 320 // The check for `eventPath != trackedPath` is to 321 // avoid a duplicate creation event (because at the 322 // top of the loop body, we forward any event on 323 // the trackedPath unconditionally) 324 if eventPath != trackedPath { 325 w.sendEvent(Event{ 326 Name: trackedPath, 327 Op: fsnotify.Create, 328 }) 329 } 330 } 331 } 332 } 333 } 334 case err := <-w.watcher.Errors: 335 log.WithError(err).Debug("Received fsnotify error while watching") 336 w.sendError(err) 337 case <-w.stop: 338 err := w.watcher.Close() 339 if err != nil { 340 log.WithError(err).Warn("Received fsnotify error on close") 341 } 342 close(w.Events) 343 close(w.Errors) 344 return 345 } 346 } 347 } 348 349 func (w *Watcher) sendEvent(e Event) { 350 select { 351 case w.Events <- e: 352 case <-w.stop: 353 } 354 } 355 356 func (w *Watcher) sendError(err error) { 357 select { 358 case w.Errors <- err: 359 case <-w.stop: 360 } 361 }