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 }