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 }