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  }