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  }