diff --git a/README.md b/README.md index 20d15a8..f2bfeef 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ the env var `GO111MODULE=on`, which enables you to develop outside of |`-exclude=…` | none | Exclude files matching this glob pattern, e.g. ".#*" ignores emacs temporary files. You may have multiples of this flag.| |`-include=…` | none | Include files whose last path component matches this glob pattern. You may have multiples of this flag.| |`-pattern=…` | (.+\\.go|.+\\.c)$ | A regular expression which matches the files to watch. The default watches *.go* and *.c* files.| +| | | **file watch** | +|`-polling=…` | false | Use polling instead of FS notifications to detect changes. Default is false +|`-polling-interval=…` | 100 | Milliseconds of interval between polling file changes when polling option is selected | | | **misc** | |`-color=_` | false | Colorize the output of the daemon's status messages. | |`-log-prefix=_` | true | Prefix all child process output with stdout/stderr labels and log timestamps. | diff --git a/daemon.go b/daemon.go index a560170..f83f7ac 100644 --- a/daemon.go +++ b/daemon.go @@ -46,6 +46,10 @@ There are command line options. -include=XXX – Include files whose basename matches glob pattern XXX -pattern=XXX – Include files whose path matches regexp XXX + FILE WATCH + -polling - Use polling instead of FS notifications to detect changes. Default is false + -polling-interval - Milliseconds of interval between polling file changes when polling option is selected + MISC -color - Enable colorized output -log-prefix - Enable/disable stdout/stderr labelling for the child process @@ -73,11 +77,9 @@ import ( "path/filepath" "regexp" "strings" - "syscall" "time" "github.com/fatih/color" - "github.com/fsnotify/fsnotify" ) // Milliseconds to wait for the next job to begin after a file change @@ -119,6 +121,8 @@ var ( flagGracefulKill = flag.Bool("graceful-kill", false, "Gracefully attempt to kill the child process by sending a SIGTERM first") flagGracefulTimeout = flag.Uint("graceful-timeout", 3, "Duration (in seconds) to wait for graceful kill to complete") flagVerbose = flag.Bool("verbose", false, "Be verbose about which directories are watched.") + flagPolling = flag.Bool("polling", false, "Use polling method to watch file change instead of fsnotify") + flagPollingInterval = flag.Int("polling-interval", 100, "Milliseconds of interval between polling file changes when polling option is selected") // initialized in main() due to custom type. flagDirectories globList @@ -386,7 +390,20 @@ func main() { log.Fatal("Graceful termination is not supported on your platform.") } - watcher, err := fsnotify.NewWatcher() + pattern := regexp.MustCompile(*flagPattern) + + cfg := &WatcherConfig{ + flagVerbose: *flagVerbose, + flagRecursive: *flagRecursive, + flagPolling: *flagPolling, + flagPollingInterval: *flagPollingInterval, + flagDirectories: flagDirectories, + flagExcludedDirs: flagExcludedDirs, + flagExcludedFiles: flagExcludedFiles, + flagIncludedFiles: flagIncludedFiles, + pattern: pattern, + } + watcher, err := NewWatcher(cfg) if err != nil { log.Fatal(err) @@ -394,37 +411,11 @@ func main() { defer watcher.Close() - for _, flagDirectory := range flagDirectories { - if *flagRecursive == true { - err = filepath.Walk(flagDirectory, func(path string, info os.FileInfo, err error) error { - if err == nil && info.IsDir() { - if flagExcludedDirs.Matches(path) { - return filepath.SkipDir - } else { - if *flagVerbose { - log.Printf("Watching directory '%s' for changes.\n", path) - } - return watcher.Add(path) - } - } - return err - }) - - if err != nil { - log.Fatal("filepath.Walk():", err) - } - - if err := watcher.Add(flagDirectory); err != nil { - log.Fatal("watcher.Add():", err) - } - } else { - if err := watcher.Add(flagDirectory); err != nil { - log.Fatal("watcher.Add():", err) - } - } + err = watcher.AddFiles() + if err != nil { + log.Fatal("watcher.Addfiles():", err) } - pattern := regexp.MustCompile(*flagPattern) jobs := make(chan string) buildSuccess := make(chan bool) buildStarted := make(chan string) @@ -437,32 +428,5 @@ func main() { go flusher(buildStarted, buildSuccess) } - for { - select { - case ev := <-watcher.Events: - if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create { - base := filepath.Base(ev.Name) - - // Assume it is a directory and track it. - if *flagRecursive == true && !flagExcludedDirs.Matches(ev.Name) { - watcher.Add(ev.Name) - } - - if flagIncludedFiles.Matches(base) || matchesPattern(pattern, ev.Name) { - if !flagExcludedFiles.Matches(base) { - jobs <- ev.Name - } - } - } - - case err := <-watcher.Errors: - if v, ok := err.(*os.SyscallError); ok { - if v.Err == syscall.EINTR { - continue - } - log.Fatal("watcher.Error: SyscallError:", v) - } - log.Fatal("watcher.Error:", err) - } - } + watcher.Watch(jobs) // start watching files } diff --git a/go.mod b/go.mod index 7820927..fbdb0ff 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.11 require ( github.com/fatih/color v1.9.0 github.com/fsnotify/fsnotify v1.4.9 + github.com/radovskyb/watcher v1.0.7 ) diff --git a/go.sum b/go.sum index 3854df0..897f9de 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= +github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= diff --git a/watcher.go b/watcher.go new file mode 100644 index 0000000..35944d1 --- /dev/null +++ b/watcher.go @@ -0,0 +1,202 @@ +package main + +import ( + "errors" + "fmt" + "github.com/fsnotify/fsnotify" + pollingWatcher "github.com/radovskyb/watcher" + "log" + "os" + "path/filepath" + "regexp" + "syscall" + "time" +) + +func directoryShouldBeTracked(cfg *WatcherConfig, path string) bool { + return cfg.flagRecursive == true && !cfg.flagExcludedDirs.Matches(path) +} + +func pathMatches(cfg *WatcherConfig, path string) bool { + base := filepath.Base(path) + return (cfg.flagIncludedFiles.Matches(base) || matchesPattern(cfg.pattern, path)) && + !cfg.flagExcludedFiles.Matches(base) +} + +type WatcherConfig struct { + flagVerbose bool + flagPolling bool + flagRecursive bool + flagPollingInterval int + flagDirectories globList + flagExcludedDirs globList + flagExcludedFiles globList + flagIncludedFiles globList + pattern *regexp.Regexp +} + +type FileWatcher interface { + Close() error + AddFiles() error + add(path string) error + Watch(jobs chan<- string) + getConfig() *WatcherConfig +} + +type NotifyWatcher struct { + watcher *fsnotify.Watcher + cfg *WatcherConfig +} + +func (n NotifyWatcher) Close() error { + return n.watcher.Close() +} + +func (n NotifyWatcher) AddFiles() error { + return addFiles(n) +} + +func (n NotifyWatcher) Watch(jobs chan<- string) { + for { + select { + case ev := <-n.watcher.Events: + if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create { + // Assume it is a directory and track it. + if directoryShouldBeTracked(n.cfg, ev.Name) { + n.watcher.Add(ev.Name) + } + if pathMatches(n.cfg, ev.Name) { + jobs <- ev.Name + } + } + + case err := <-n.watcher.Errors: + if v, ok := err.(*os.SyscallError); ok { + if v.Err == syscall.EINTR { + continue + } + log.Fatal("watcher.Error: SyscallError:", v) + } + log.Fatal("watcher.Error:", err) + } + } +} + +func (n NotifyWatcher) add(path string) error { + return n.watcher.Add(path) +} + +func (n NotifyWatcher) getConfig() *WatcherConfig { + return n.cfg +} + +type PollingWatcher struct { + watcher *pollingWatcher.Watcher + cfg *WatcherConfig +} + +func (p PollingWatcher) Close() error { + p.watcher.Close() + return nil +} + +func (p PollingWatcher) AddFiles() error { + p.watcher.AddFilterHook(pollingWatcher.RegexFilterHook(p.cfg.pattern, false)) + + return addFiles(p) +} + +func (p PollingWatcher) Watch(jobs chan<- string) { + // Start the watching process. + go func() { + if err := p.watcher.Start(time.Duration(p.cfg.flagPollingInterval) * time.Millisecond); err != nil { + log.Fatalln(err) + } + }() + + for { + select { + case event := <-p.watcher.Event: + if p.cfg.flagVerbose { + // Print the event's info. + fmt.Println(event) + } + + if pathMatches(p.cfg, event.Path) { + jobs <- event.String() + } + case err := <-p.watcher.Error: + if err == pollingWatcher.ErrWatchedFileDeleted { + continue + } + log.Fatalln(err) + case <-p.watcher.Closed: + return + } + } +} + +func (p PollingWatcher) add(path string) error { + return p.watcher.Add(path) +} + +func (p PollingWatcher) getConfig() *WatcherConfig { + return p.cfg +} + +func NewWatcher(cfg *WatcherConfig) (FileWatcher, error) { + if cfg == nil { + err := errors.New("no config specified") + return nil, err + } + if cfg.flagPolling { + w := pollingWatcher.New() + return PollingWatcher{ + watcher: w, + cfg: cfg, + }, nil + } else { + w, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + return NotifyWatcher{ + watcher: w, + cfg: cfg, + }, nil + } +} + +func addFiles(fw FileWatcher) error { + cfg := fw.getConfig() + for _, flagDirectory := range cfg.flagDirectories { + if cfg.flagRecursive == true { + err := filepath.Walk(flagDirectory, func(path string, info os.FileInfo, err error) error { + if err == nil && info.IsDir() { + if cfg.flagExcludedDirs.Matches(path) { + return filepath.SkipDir + } else { + if cfg.flagVerbose { + log.Printf("Watching directory '%s' for changes.\n", path) + } + return fw.add(path) + } + } + return err + }) + + if err != nil { + return fmt.Errorf("filepath.Walk(): %v", err) + } + + if err := fw.add(flagDirectory); err != nil { + return fmt.Errorf("FileWatcher.Add(): %v", err) + } + } else { + if err := fw.add(flagDirectory); err != nil { + return fmt.Errorf("FileWatcher.AddFiles(): %v", err) + } + } + } + return nil +}