github.com/netdata/go.d.plugin@v0.58.1/agent/discovery/file/watch.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package file 4 5 import ( 6 "context" 7 "os" 8 "path/filepath" 9 "strings" 10 "time" 11 12 "github.com/netdata/go.d.plugin/agent/confgroup" 13 "github.com/netdata/go.d.plugin/logger" 14 15 "github.com/fsnotify/fsnotify" 16 ) 17 18 type ( 19 Watcher struct { 20 *logger.Logger 21 22 paths []string 23 reg confgroup.Registry 24 watcher *fsnotify.Watcher 25 cache cache 26 refreshEvery time.Duration 27 } 28 cache map[string]time.Time 29 ) 30 31 func (c cache) lookup(path string) (time.Time, bool) { v, ok := c[path]; return v, ok } 32 func (c cache) has(path string) bool { _, ok := c.lookup(path); return ok } 33 func (c cache) remove(path string) { delete(c, path) } 34 func (c cache) put(path string, modTime time.Time) { c[path] = modTime } 35 36 func NewWatcher(reg confgroup.Registry, paths []string) *Watcher { 37 d := &Watcher{ 38 Logger: log, 39 paths: paths, 40 reg: reg, 41 watcher: nil, 42 cache: make(cache), 43 refreshEvery: time.Minute, 44 } 45 return d 46 } 47 48 func (w *Watcher) String() string { 49 return w.Name() 50 } 51 52 func (w *Watcher) Name() string { 53 return "file watcher" 54 } 55 56 func (w *Watcher) Run(ctx context.Context, in chan<- []*confgroup.Group) { 57 w.Info("instance is started") 58 defer func() { w.Info("instance is stopped") }() 59 60 watcher, err := fsnotify.NewWatcher() 61 if err != nil { 62 w.Errorf("fsnotify watcher initialization: %v", err) 63 return 64 } 65 66 w.watcher = watcher 67 defer w.stop() 68 w.refresh(ctx, in) 69 70 tk := time.NewTicker(w.refreshEvery) 71 defer tk.Stop() 72 73 for { 74 select { 75 case <-ctx.Done(): 76 return 77 case <-tk.C: 78 w.refresh(ctx, in) 79 case event := <-w.watcher.Events: 80 // TODO: check if event.Has will do 81 if event.Name == "" || isChmodOnly(event) || !w.fileMatches(event.Name) { 82 break 83 } 84 if event.Has(fsnotify.Create) && w.cache.has(event.Name) { 85 // vim "backupcopy=no" case, already collected after Rename event. 86 break 87 } 88 if event.Has(fsnotify.Rename) { 89 // It is common to modify files using vim. 90 // When writing to a file a backup is made. "backupcopy" option tells how it's done. 91 // Default is "no": rename the file and write a new one. 92 // This is cheap attempt to not send empty group for the old file. 93 time.Sleep(time.Millisecond * 100) 94 } 95 w.refresh(ctx, in) 96 case err := <-w.watcher.Errors: 97 if err != nil { 98 w.Warningf("watch: %v", err) 99 } 100 } 101 } 102 } 103 104 func (w *Watcher) fileMatches(file string) bool { 105 for _, pattern := range w.paths { 106 if ok, _ := filepath.Match(pattern, file); ok { 107 return true 108 } 109 } 110 return false 111 } 112 113 func (w *Watcher) listFiles() (files []string) { 114 for _, pattern := range w.paths { 115 if matches, err := filepath.Glob(pattern); err == nil { 116 files = append(files, matches...) 117 } 118 } 119 return files 120 } 121 122 func (w *Watcher) refresh(ctx context.Context, in chan<- []*confgroup.Group) { 123 select { 124 case <-ctx.Done(): 125 return 126 default: 127 } 128 var groups []*confgroup.Group 129 seen := make(map[string]bool) 130 131 for _, file := range w.listFiles() { 132 fi, err := os.Lstat(file) 133 if err != nil { 134 w.Warningf("lstat '%s': %v", file, err) 135 continue 136 } 137 138 if !fi.Mode().IsRegular() { 139 continue 140 } 141 142 seen[file] = true 143 if v, ok := w.cache.lookup(file); ok && v.Equal(fi.ModTime()) { 144 continue 145 } 146 w.cache.put(file, fi.ModTime()) 147 148 if group, err := parse(w.reg, file); err != nil { 149 w.Warningf("parse '%s': %v", file, err) 150 } else if group == nil { 151 groups = append(groups, &confgroup.Group{Source: file}) 152 } else { 153 groups = append(groups, group) 154 } 155 } 156 157 for name := range w.cache { 158 if seen[name] { 159 continue 160 } 161 w.cache.remove(name) 162 groups = append(groups, &confgroup.Group{Source: name}) 163 } 164 165 for _, group := range groups { 166 for _, cfg := range group.Configs { 167 cfg.SetSource(group.Source) 168 cfg.SetProvider("file watcher") 169 } 170 } 171 172 send(ctx, in, groups) 173 w.watchDirs() 174 } 175 176 func (w *Watcher) watchDirs() { 177 for _, path := range w.paths { 178 if idx := strings.LastIndex(path, "/"); idx > -1 { 179 path = path[:idx] 180 } else { 181 path = "./" 182 } 183 if err := w.watcher.Add(path); err != nil { 184 w.Errorf("start watching '%s': %v", path, err) 185 } 186 } 187 } 188 189 func (w *Watcher) stop() { 190 ctx, cancel := context.WithCancel(context.Background()) 191 defer cancel() 192 193 // closing the watcher deadlocks unless all events and errors are drained. 194 go func() { 195 for { 196 select { 197 case <-w.watcher.Errors: 198 case <-w.watcher.Events: 199 case <-ctx.Done(): 200 return 201 } 202 } 203 }() 204 205 // in fact never returns an error 206 _ = w.watcher.Close() 207 } 208 209 func isChmodOnly(event fsnotify.Event) bool { 210 return event.Op^fsnotify.Chmod == 0 211 } 212 213 func send(ctx context.Context, in chan<- []*confgroup.Group, groups []*confgroup.Group) { 214 if len(groups) == 0 { 215 return 216 } 217 select { 218 case <-ctx.Done(): 219 case in <- groups: 220 } 221 }