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