github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/aagent/watchers/filewatcher/file.go (about)

     1  // Copyright (c) 2019-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package filewatcher
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/choria-io/go-choria/aagent/model"
    16  	"github.com/choria-io/go-choria/aagent/util"
    17  	"github.com/choria-io/go-choria/aagent/watchers/event"
    18  	"github.com/choria-io/go-choria/aagent/watchers/watcher"
    19  	iu "github.com/choria-io/go-choria/internal/util"
    20  )
    21  
    22  type State int
    23  
    24  const (
    25  	Unknown State = iota
    26  	Error
    27  	Skipped
    28  	Unchanged
    29  	Changed
    30  
    31  	wtype   = "file"
    32  	version = "v1"
    33  )
    34  
    35  var stateNames = map[State]string{
    36  	Unknown:   "unknown",
    37  	Error:     "error",
    38  	Skipped:   "skipped",
    39  	Unchanged: "unchanged",
    40  	Changed:   "changed",
    41  }
    42  
    43  type Properties struct {
    44  	Path    string
    45  	Initial bool `mapstructure:"gather_initial_state"`
    46  }
    47  
    48  type Watcher struct {
    49  	*watcher.Watcher
    50  
    51  	name       string
    52  	machine    model.Machine
    53  	previous   State
    54  	interval   time.Duration
    55  	mtime      time.Time
    56  	properties *Properties
    57  	mu         *sync.Mutex
    58  }
    59  
    60  func New(machine model.Machine, name string, states []string, failEvent string, successEvent string, interval string, ai time.Duration, properties map[string]any) (any, error) {
    61  	var err error
    62  
    63  	fw := &Watcher{
    64  		properties: &Properties{},
    65  		name:       name,
    66  		machine:    machine,
    67  		interval:   5 * time.Second,
    68  		mu:         &sync.Mutex{},
    69  	}
    70  
    71  	fw.Watcher, err = watcher.NewWatcher(name, wtype, ai, states, machine, failEvent, successEvent)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	err = fw.setProperties(properties)
    77  	if err != nil {
    78  		return nil, fmt.Errorf("could not set properties: %s", err)
    79  	}
    80  
    81  	if !filepath.IsAbs(fw.properties.Path) {
    82  		fw.properties.Path = filepath.Join(fw.machine.Directory(), fw.properties.Path)
    83  	}
    84  
    85  	if interval != "" {
    86  		fw.interval, err = iu.ParseDuration(interval)
    87  		if err != nil {
    88  			return nil, fmt.Errorf("invalid interval: %s", err)
    89  		}
    90  	}
    91  
    92  	if fw.interval < 500*time.Millisecond {
    93  		return nil, fmt.Errorf("interval %v is too small", fw.interval)
    94  	}
    95  
    96  	return fw, err
    97  }
    98  
    99  func (w *Watcher) Run(ctx context.Context, wg *sync.WaitGroup) {
   100  	defer wg.Done()
   101  
   102  	w.Infof("file watcher for %s starting", w.properties.Path)
   103  
   104  	tick := time.NewTicker(w.interval)
   105  
   106  	if w.properties.Initial {
   107  		stat, err := os.Stat(w.properties.Path)
   108  		if err == nil {
   109  			w.mtime = stat.ModTime()
   110  		}
   111  	}
   112  
   113  	for {
   114  		select {
   115  		case <-tick.C:
   116  			w.performWatch(ctx)
   117  
   118  		case <-w.Watcher.StateChangeC():
   119  			w.performWatch(ctx)
   120  
   121  		case <-ctx.Done():
   122  			tick.Stop()
   123  			w.Infof("Stopping on context interrupt")
   124  			return
   125  		}
   126  	}
   127  }
   128  
   129  func (w *Watcher) performWatch(_ context.Context) {
   130  	state, err := w.watch()
   131  	err = w.handleCheck(state, err)
   132  	if err != nil {
   133  		w.Errorf("could not handle watcher event: %s", err)
   134  	}
   135  }
   136  
   137  func (w *Watcher) CurrentState() any {
   138  	w.mu.Lock()
   139  	defer w.mu.Unlock()
   140  
   141  	s := &StateNotification{
   142  		Event:           event.New(w.name, wtype, version, w.machine),
   143  		Path:            w.properties.Path,
   144  		PreviousOutcome: stateNames[w.previous],
   145  	}
   146  
   147  	return s
   148  }
   149  
   150  func (w *Watcher) setPreviousState(s State) {
   151  	w.mu.Lock()
   152  	defer w.mu.Unlock()
   153  
   154  	w.previous = s
   155  }
   156  
   157  func (w *Watcher) handleCheck(s State, err error) error {
   158  	w.Debugf("Handling check for %s %v %v", w.properties.Path, s, err)
   159  
   160  	w.setPreviousState(s)
   161  
   162  	switch s {
   163  	case Error:
   164  		w.NotifyWatcherState(w.CurrentState())
   165  		return w.FailureTransition()
   166  
   167  	case Changed:
   168  		w.NotifyWatcherState(w.CurrentState())
   169  		return w.SuccessTransition()
   170  
   171  	case Unchanged:
   172  	// not notifying, regular announces happen
   173  
   174  	case Skipped:
   175  	// nothing really to do, we keep old mtime next time round
   176  	// we'll correctly notify of changes
   177  
   178  	case Unknown:
   179  		w.mtime = time.Time{}
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  func (w *Watcher) watch() (state State, err error) {
   186  	if !w.Watcher.ShouldWatch() {
   187  		return Skipped, nil
   188  	}
   189  
   190  	stat, err := os.Stat(w.properties.Path)
   191  	if err != nil {
   192  		w.mtime = time.Time{}
   193  		return Error, fmt.Errorf("does not exist")
   194  	}
   195  
   196  	if stat.ModTime().After(w.mtime) {
   197  		w.mtime = stat.ModTime()
   198  		return Changed, nil
   199  	}
   200  
   201  	return Unchanged, err
   202  }
   203  
   204  func (w *Watcher) validate() error {
   205  	if w.properties.Path == "" {
   206  		return fmt.Errorf("path is required")
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func (w *Watcher) setProperties(props map[string]any) error {
   213  	if w.properties == nil {
   214  		w.properties = &Properties{}
   215  	}
   216  
   217  	err := util.ParseMapStructure(props, w.properties)
   218  	if err != nil {
   219  		return err
   220  	}
   221  
   222  	return w.validate()
   223  }