recwatch: Split recursive watching into sub-package

This splits the recursive watching bit of the file file resource into
it's own package. This also de-duplicates the configwatch code and puts
it into the same package. With these bits refactored, it was also easier
to clean up the error code in main.
This commit is contained in:
James Shubin
2016-10-03 14:48:57 -04:00
parent 567dcaf79d
commit 46893e84c3
5 changed files with 514 additions and 437 deletions

View File

@@ -26,20 +26,16 @@ import (
"io"
"io/ioutil"
"log"
"math"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/purpleidea/mgmt/event"
"github.com/purpleidea/mgmt/global" // XXX: package mgmtmain instead?
"github.com/purpleidea/mgmt/recwatch"
"github.com/purpleidea/mgmt/util"
"gopkg.in/fsnotify.v1"
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
)
func init() {
@@ -48,20 +44,19 @@ func init() {
// FileRes is a file and directory resource.
type FileRes struct {
BaseRes `yaml:",inline"`
Path string `yaml:"path"` // path variable (should default to name)
Dirname string `yaml:"dirname"`
Basename string `yaml:"basename"`
Content string `yaml:"content"` // FIXME: how do you describe: "leave content alone" - state = "create" ?
Source string `yaml:"source"` // file path for source content
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
Recurse bool `yaml:"recurse"`
Force bool `yaml:"force"`
path string // computed path
isDir bool // computed isDir
sha256sum string
watcher *fsnotify.Watcher
watches map[string]struct{}
BaseRes `yaml:",inline"`
Path string `yaml:"path"` // path variable (should default to name)
Dirname string `yaml:"dirname"`
Basename string `yaml:"basename"`
Content string `yaml:"content"` // FIXME: how do you describe: "leave content alone" - state = "create" ?
Source string `yaml:"source"` // file path for source content
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
Recurse bool `yaml:"recurse"`
Force bool `yaml:"force"`
path string // computed path
isDir bool // computed isDir
sha256sum string
recWatcher *recwatch.RecWatcher
}
// NewFileRes is a constructor for this resource. It also calls Init() for you.
@@ -86,7 +81,6 @@ func NewFileRes(name, path, dirname, basename, content, source, state string, re
// Init runs some startup code for this resource.
func (obj *FileRes) Init() error {
obj.sha256sum = ""
obj.watches = make(map[string]struct{})
if obj.Path == "" { // use the name as the path default if missing
obj.Path = obj.BaseRes.Name
}
@@ -141,32 +135,6 @@ func (obj *FileRes) Validate() error {
return nil
}
// addSubFolders is a helper that is used to add recursive dirs to the watches.
func (obj *FileRes) addSubFolders(p string) error {
if !obj.Recurse {
return nil // if we're not watching recursively, just exit early
}
// look at all subfolders...
walkFn := func(path string, info os.FileInfo, err error) error {
if global.DEBUG {
log.Printf("%s[%s]: Walk: %s (%v): %v", obj.Kind(), obj.GetName(), path, info, err)
}
if err != nil {
return nil
}
if info.IsDir() {
obj.watches[path] = struct{}{} // add key
err := obj.watcher.Add(path)
if err != nil {
return err // TODO: will this bubble up?
}
}
return nil
}
err := filepath.Walk(p, walkFn)
return err
}
// Watch is the primary listener for this resource and it outputs events.
// This one is a file watcher for files and directories.
// Modify with caution, it is probably important to write some test cases first!
@@ -191,167 +159,37 @@ func (obj *FileRes) Watch(processChan chan event.Event) error {
return time.After(time.Duration(500) * time.Millisecond) // 1/2 the resolution of converged timeout
}
var safename = path.Clean(obj.path) // no trailing slash
var err error
obj.watcher, err = fsnotify.NewWatcher()
obj.recWatcher, err = recwatch.NewRecWatcher(obj.Path, obj.Recurse)
if err != nil {
return err
}
defer obj.watcher.Close()
defer obj.recWatcher.Close()
patharray := util.PathSplit(safename) // tokenize the path
var index = len(patharray) // starting index
var current string // current "watcher" location
var deltaDepth int // depth delta between watcher and event
var send = false // send event?
var send = false // send event?
var exit = false
var dirty = false
isDir := func(p string) bool {
finfo, err := os.Stat(p)
if err != nil {
return false
}
return finfo.IsDir()
}
if obj.isDir {
if err := obj.addSubFolders(safename); err != nil {
return err
}
}
for {
current = strings.Join(patharray[0:index], "/")
if current == "" { // the empty string top is the root dir ("/")
current = "/"
}
if global.DEBUG {
log.Printf("%s[%s]: Watching: %v", obj.Kind(), obj.GetName(), current) // attempting to watch...
}
// initialize in the loop so that we can reset on rm-ed handles
err = obj.watcher.Add(current)
if err != nil {
if global.DEBUG {
log.Printf("%s[%s]: watcher.Add(%v): Error: %v", obj.Kind(), obj.GetName(), current, err)
}
if err == syscall.ENOENT {
index-- // usually not found, move up one dir
} else if err == syscall.ENOSPC {
// no space left on device, out of inotify watches
// TODO: consider letting the user fall back to
// polling if they hit this error very often...
return fmt.Errorf("%s[%s]: Out of inotify watches: %v", obj.Kind(), obj.GetName(), err)
} else if os.IsPermission(err) {
return fmt.Errorf("%s[%s]: Permission denied to add a watch: %v", obj.Kind(), obj.GetName(), err)
} else {
return fmt.Errorf("Unknown %s[%s] error: %v", obj.Kind(), obj.GetName(), err)
}
index = int(math.Max(1, float64(index)))
continue
log.Printf("%s[%s]: Watching: %s", obj.Kind(), obj.GetName(), obj.Path) // attempting to watch...
}
obj.SetState(ResStateWatching) // reset
select {
case event := <-obj.watcher.Events:
if global.DEBUG {
log.Printf("%s[%s]: Watch(%s), Event(%s): %v", obj.Kind(), obj.GetName(), current, event.Name, event.Op)
case event, ok := <-obj.recWatcher.Events():
if !ok { // channel shutdown
return nil
}
cuuid.SetConverged(false) // XXX: technically i can detect if the event is erroneous or not first
// the deeper you go, the bigger the deltaDepth is...
// this is the difference between what we're watching,
// and the event... doesn't mean we can't watch deeper
if current == event.Name {
deltaDepth = 0 // i was watching what i was looking for
} else if util.HasPathPrefix(event.Name, current) {
deltaDepth = len(util.PathSplit(current)) - len(util.PathSplit(event.Name)) // -1 or less
} else if util.HasPathPrefix(current, event.Name) {
deltaDepth = len(util.PathSplit(event.Name)) - len(util.PathSplit(current)) // +1 or more
// if below me...
if _, exists := obj.watches[event.Name]; exists {
send = true
dirty = true
if event.Op&fsnotify.Remove == fsnotify.Remove {
obj.watcher.Remove(event.Name)
delete(obj.watches, event.Name)
}
if (event.Op&fsnotify.Create == fsnotify.Create) && isDir(event.Name) {
obj.watcher.Add(event.Name)
obj.watches[event.Name] = struct{}{}
if err := obj.addSubFolders(event.Name); err != nil {
return err
}
}
}
} else {
// TODO different watchers get each others events!
// https://github.com/go-fsnotify/fsnotify/issues/95
// this happened with two values such as:
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
continue
}
//log.Printf("The delta depth is: %v", deltaDepth)
// if we have what we wanted, awesome, send an event...
if event.Name == safename {
//log.Println("Event!")
// FIXME: should all these below cases trigger?
send = true
dirty = true
if obj.isDir {
if err := obj.addSubFolders(safename); err != nil {
return err
}
}
// file removed, move the watch upwards
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
//log.Println("Removal!")
obj.watcher.Remove(current)
index--
}
// we must be a parent watcher, so descend in
if deltaDepth < 0 {
// XXX: we can block here due to: https://github.com/fsnotify/fsnotify/issues/123
obj.watcher.Remove(current)
index++
}
// if safename starts with event.Name, we're above, and no event should be sent
} else if util.HasPathPrefix(safename, event.Name) {
//log.Println("Above!")
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
log.Println("Removal!")
obj.watcher.Remove(current)
index--
}
if deltaDepth < 0 {
log.Println("Parent!")
if util.PathPrefixDelta(safename, event.Name) == 1 { // we're the parent dir
send = true
dirty = true
}
obj.watcher.Remove(current)
index++
}
// if event.Name startswith safename, send event, we're already deeper
} else if util.HasPathPrefix(event.Name, safename) {
//log.Println("Event2!")
send = true
dirty = true
}
case err := <-obj.watcher.Errors:
cuuid.SetConverged(false)
return fmt.Errorf("Unknown %s[%s] watcher error: %v", obj.Kind(), obj.GetName(), err)
if err := event.Error; err != nil {
return fmt.Errorf("Unknown %s[%s] watcher error: %v", obj.Kind(), obj.GetName(), err)
}
if global.DEBUG { // don't access event.Body if event.Error isn't nil
log.Printf("%s[%s]: Event(%s): %v", obj.Kind(), obj.GetName(), event.Body.Name, event.Body.Op)
}
send = true
dirty = true
case event := <-obj.events:
cuuid.SetConverged(false)