github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/filenotifywatcher/watcher.go (about)

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package filenotifywatcher
     5  
     6  import (
     7  	"path/filepath"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  	"github.com/juju/worker/v3"
    12  	"github.com/juju/worker/v3/catacomb"
    13  	"k8s.io/utils/inotify"
    14  )
    15  
    16  const (
    17  	defaultWatcherPath = "/var/lib/juju/locks"
    18  )
    19  
    20  // FileWatcher is an interface that allows a worker to watch a file for changes.
    21  type FileWatcher interface {
    22  	worker.Worker
    23  	// Changes returns a channel that will receive a value whenever the
    24  	// watched file changes.
    25  	Changes() <-chan bool
    26  }
    27  
    28  // INotifyWatcher is an interface that allows a worker to watch a file for
    29  // changes using inotify.
    30  type INotifyWatcher interface {
    31  	// Watch adds the given file or directory (non-recursively) to the watch.
    32  	Watch(path string) error
    33  
    34  	// Events returns the next event.
    35  	Events() <-chan *inotify.Event
    36  
    37  	// Errors returns the next error.
    38  	Errors() <-chan error
    39  
    40  	// Close removes all watches and closes the events channel.
    41  	Close() error
    42  }
    43  
    44  type option struct {
    45  	path      string
    46  	logger    Logger
    47  	watcherFn func() (INotifyWatcher, error)
    48  }
    49  
    50  type Option func(*option)
    51  
    52  // WithPath is an option for NewWatcher that specifies the path to watch.
    53  func WithPath(path string) Option {
    54  	return func(o *option) {
    55  		o.path = path
    56  	}
    57  }
    58  
    59  // WithLogger is an option for NewWatcher that specifies the logger to use.
    60  func WithLogger(logger Logger) Option {
    61  	return func(o *option) {
    62  		o.logger = logger
    63  	}
    64  }
    65  
    66  // WithINotifyWatcherFn is an option for NewWatcher that specifies the inotify
    67  // watcher to use.
    68  func WithINotifyWatcherFn(watcherFn func() (INotifyWatcher, error)) Option {
    69  	return func(o *option) {
    70  		o.watcherFn = watcherFn
    71  	}
    72  }
    73  
    74  func newOption() *option {
    75  	return &option{
    76  		path:      defaultWatcherPath,
    77  		logger:    loggo.GetLogger("juju.worker.filenotifywatcher"),
    78  		watcherFn: newWatcher,
    79  	}
    80  }
    81  
    82  // NewInotifyWatcher returns a new INotifyWatcher.
    83  var NewINotifyWatcher = newWatcher
    84  
    85  type Watcher struct {
    86  	catacomb catacomb.Catacomb
    87  
    88  	fileName string
    89  	changes  chan bool
    90  
    91  	watchPath string
    92  	watcher   INotifyWatcher
    93  
    94  	logger Logger
    95  }
    96  
    97  func NewWatcher(fileName string, opts ...Option) (FileWatcher, error) {
    98  	o := newOption()
    99  	for _, opt := range opts {
   100  		opt(o)
   101  	}
   102  
   103  	watcher, err := o.watcherFn()
   104  	if err != nil {
   105  		return nil, errors.Annotatef(err, "creating watcher for file %q in path %q", fileName, o.path)
   106  	}
   107  	if err := watcher.Watch(o.path); err != nil {
   108  		return nil, errors.Annotatef(err, "watching file %q in path %q", fileName, o.path)
   109  	}
   110  
   111  	w := &Watcher{
   112  		fileName:  fileName,
   113  		changes:   make(chan bool),
   114  		watcher:   watcher,
   115  		watchPath: filepath.Join(o.path, fileName),
   116  		logger:    o.logger,
   117  	}
   118  
   119  	if err := catacomb.Invoke(catacomb.Plan{
   120  		Site: &w.catacomb,
   121  		Work: w.loop,
   122  	}); err != nil {
   123  		return nil, errors.Trace(err)
   124  	}
   125  
   126  	return w, nil
   127  }
   128  
   129  // Kill is part of the worker.Worker interface.
   130  func (w *Watcher) Kill() {
   131  	w.catacomb.Kill(nil)
   132  }
   133  
   134  // Wait is part of the worker.Worker interface.
   135  func (w *Watcher) Wait() error {
   136  	return w.catacomb.Wait()
   137  }
   138  
   139  // Changes returns the changes for the given fileName.
   140  func (w *Watcher) Changes() <-chan bool {
   141  	return w.changes
   142  }
   143  
   144  func (w *Watcher) loop() error {
   145  	defer func() {
   146  		_ = w.watcher.Close()
   147  		close(w.changes)
   148  	}()
   149  
   150  	for {
   151  		select {
   152  		case <-w.catacomb.Dying():
   153  			return w.catacomb.ErrDying()
   154  		case event := <-w.watcher.Events():
   155  			if w.logger.IsTraceEnabled() {
   156  				w.logger.Tracef("inotify event for %v", event)
   157  			}
   158  			// Ignore events for other files in the directory.
   159  			if event.Name != w.watchPath {
   160  				continue
   161  			}
   162  			// If the event is not a create or delete event, ignore it.
   163  			if maskType(event.Mask) == unknown {
   164  				continue
   165  			}
   166  
   167  			created := event.Mask&inotify.InCreate != 0
   168  
   169  			if w.logger.IsTraceEnabled() {
   170  				w.logger.Tracef("dispatch event for fileName %q: %v", w.fileName, event)
   171  			}
   172  
   173  			w.changes <- created
   174  
   175  		case err := <-w.watcher.Errors():
   176  			w.logger.Errorf("error watching fileName %q with %v", w.fileName, err)
   177  		}
   178  	}
   179  }
   180  
   181  // eventType normalizes the inotify event type, to known types.
   182  type eventType int
   183  
   184  const (
   185  	unknown eventType = iota
   186  	created
   187  	deleted
   188  )
   189  
   190  // makeType returns the event type for the given mask.
   191  // It expects that created and deleted can never be set at the same time.
   192  func maskType(m uint32) eventType {
   193  	if m&inotify.InCreate != 0 {
   194  		return created
   195  	}
   196  	if m&inotify.InDelete != 0 {
   197  		return deleted
   198  	}
   199  	return unknown
   200  }