From 3fc80559b65ddb076b1e49b26761dbf99b170b89 Mon Sep 17 00:00:00 2001 From: Lambert Date: Tue, 24 Nov 2020 15:47:11 +0900 Subject: [PATCH 1/9] add file watcher using polling method --- README.md | 2 + daemon.go | 75 +++++----------------- go.mod | 1 + go.sum | 2 + watcher.go | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 59 deletions(-) create mode 100644 watcher.go diff --git a/README.md b/README.md index 20d15a8..0d40609 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ 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 | Which method to detect file changes. | | | **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..9d924fe 100644 --- a/daemon.go +++ b/daemon.go @@ -46,6 +46,9 @@ 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 - Which method to detect file changes. default is false + MISC -color - Enable colorized output -log-prefix - Enable/disable stdout/stderr labelling for the child process @@ -73,16 +76,17 @@ 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 const WorkDelay = 900 +// ... +const PollingInterval = "100ms" + // Default pattern to match files which trigger a build const FilePattern = `(.+\.go|.+\.c)$` @@ -119,6 +123,7 @@ 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") // initialized in main() due to custom type. flagDirectories globList @@ -386,7 +391,9 @@ func main() { log.Fatal("Graceful termination is not supported on your platform.") } - watcher, err := fsnotify.NewWatcher() + /////////////////////////////// + + watcher, err := NewWatcher(*flagPolling) if err != nil { log.Fatal(err) @@ -394,37 +401,14 @@ 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) - } + pattern := regexp.MustCompile(*flagPattern) - 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) - } - } + // add files + err = watcher.AddFiles(pattern) + 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 +421,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, pattern) // 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..a577fdc --- /dev/null +++ b/watcher.go @@ -0,0 +1,181 @@ +package main + +import ( + "fmt" + "github.com/fsnotify/fsnotify" + pollingWatcher "github.com/radovskyb/watcher" + "log" + "os" + "path/filepath" + "regexp" + "syscall" + "time" +) + +type FileWatcher interface { + Close() error + AddFiles(pattern *regexp.Regexp) error + Watch(jobs chan<- string, pattern *regexp.Regexp) +} + +type NotifyWatcher struct { + watcher *fsnotify.Watcher +} + +func (n NotifyWatcher) Close() error { + return n.watcher.Close() +} + +func (n NotifyWatcher) AddFiles(pattern *regexp.Regexp) error { + 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 n.watcher.Add(path) + } + } + return err + }) + + if err != nil { + return fmt.Errorf("filepath.Walk(): %v", err) + } + + if err := n.watcher.Add(flagDirectory); err != nil { + return fmt.Errorf("NotifyWatcher.Add(): %v", err) + } + } else { + if err := n.watcher.Add(flagDirectory); err != nil { + return fmt.Errorf("NotifyWatcher.Add(): %v", err) + } + } + } + return nil +} + +func (n NotifyWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { + 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 { + base := filepath.Base(ev.Name) + + // Assume it is a directory and track it. + if *flagRecursive == true && !flagExcludedDirs.Matches(ev.Name) { + n.watcher.Add(ev.Name) + } + + if flagIncludedFiles.Matches(base) || matchesPattern(pattern, ev.Name) { + if !flagExcludedFiles.Matches(base) { + 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) + } + } +} + +type PollingWatcher struct { + watcher *pollingWatcher.Watcher +} + +func (p PollingWatcher) Close() error { + p.watcher.Close() + return nil +} + +func (p PollingWatcher) AddFiles(pattern *regexp.Regexp) error { + p.watcher.AddFilterHook(pollingWatcher.RegexFilterHook(pattern, false)) + + for _, flagDirectory := range flagDirectories { + if *flagRecursive == true { + if err := p.watcher.AddRecursive(flagDirectory); err != nil { + return fmt.Errorf("PollingWatcher.AddFiles(): %v", err) + } + } else { + if err := p.watcher.Add(flagDirectory); err != nil { + return fmt.Errorf("NotifyWatcher.AddFiles(): %v", err) + } + } + } + + if *flagVerbose { + for path, f := range p.watcher.WatchedFiles() { + fmt.Printf("Watching %s: %s\n", path, f.Name()) + } + } + return nil +} + +func (p PollingWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { + // Parse the interval string into a time.Duration. + parsedInterval, err := time.ParseDuration(PollingInterval) + if err != nil { + log.Fatalln(err) + } + + // Start the watching process. + go func() { + if err := p.watcher.Start(parsedInterval); err != nil { + log.Fatalln(err) + } + }() + + for { + select { + case event := <-p.watcher.Event: + if *flagVerbose { + // Print the event's info. + fmt.Println(event) + } + + base := filepath.Base(event.Path) + + if flagIncludedFiles.Matches(base) || matchesPattern(pattern, event.Path) { + if !flagExcludedFiles.Matches(base) { + jobs <- event.String() + } + } + case err := <-p.watcher.Error: + if err == pollingWatcher.ErrWatchedFileDeleted { + fmt.Println(err) + continue + } + log.Fatalln(err) + case <-p.watcher.Closed: + return + } + } +} + +func NewWatcher(usePolling bool) (FileWatcher, error) { + if usePolling { + w := pollingWatcher.New() + return PollingWatcher{ + watcher: w, + }, nil + } else { + w, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + return NotifyWatcher{ + watcher: w, + }, nil + } +} From 254db8335d4dd3b89df5e3564d89bdd99f7b2438 Mon Sep 17 00:00:00 2001 From: Lambert Date: Tue, 24 Nov 2020 15:59:02 +0900 Subject: [PATCH 2/9] change module name --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index fbdb0ff..c71a84b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/githubnemo/CompileDaemon +module github.com/daangn/CompileDaemon go 1.11 From 4b0afac98084ac95163550ef8d0a6f8e84a3f435 Mon Sep 17 00:00:00 2001 From: Lambert Date: Fri, 27 Nov 2020 14:03:40 +0900 Subject: [PATCH 3/9] fix: use filepath.Walk to filter exclude-dir properly --- daemon.go | 4 ++-- watcher.go | 38 ++++++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/daemon.go b/daemon.go index 9d924fe..86356ce 100644 --- a/daemon.go +++ b/daemon.go @@ -84,8 +84,8 @@ import ( // Milliseconds to wait for the next job to begin after a file change const WorkDelay = 900 -// ... -const PollingInterval = "100ms" +// Milliseconds of interval between polling file changes when polling option is selected +const PollingInterval = 100 // Default pattern to match files which trigger a build const FilePattern = `(.+\.go|.+\.c)$` diff --git a/watcher.go b/watcher.go index a577fdc..500b00b 100644 --- a/watcher.go +++ b/watcher.go @@ -104,34 +104,40 @@ func (p PollingWatcher) AddFiles(pattern *regexp.Regexp) error { for _, flagDirectory := range flagDirectories { if *flagRecursive == true { - if err := p.watcher.AddRecursive(flagDirectory); err != nil { - return fmt.Errorf("PollingWatcher.AddFiles(): %v", err) + 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 p.watcher.Add(path) + } + } + return err + }) + + if err != nil { + return fmt.Errorf("filepath.Walk(): %v", err) + } + + if err := p.watcher.Add(flagDirectory); err != nil { + return fmt.Errorf("PollingWatcher.Add(): %v", err) } } else { if err := p.watcher.Add(flagDirectory); err != nil { - return fmt.Errorf("NotifyWatcher.AddFiles(): %v", err) + return fmt.Errorf("PollingWatcher.AddFiles(): %v", err) } } } - - if *flagVerbose { - for path, f := range p.watcher.WatchedFiles() { - fmt.Printf("Watching %s: %s\n", path, f.Name()) - } - } return nil } func (p PollingWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { - // Parse the interval string into a time.Duration. - parsedInterval, err := time.ParseDuration(PollingInterval) - if err != nil { - log.Fatalln(err) - } - // Start the watching process. go func() { - if err := p.watcher.Start(parsedInterval); err != nil { + if err := p.watcher.Start(PollingInterval * time.Millisecond); err != nil { log.Fatalln(err) } }() From 87930581218c08ab32f338638d65f35d4e18e7b4 Mon Sep 17 00:00:00 2001 From: Lambert Date: Fri, 27 Nov 2020 14:12:11 +0900 Subject: [PATCH 4/9] chore: refactor common logics --- watcher.go | 104 +++++++++++++++++++++++------------------------------ 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/watcher.go b/watcher.go index 500b00b..33b8353 100644 --- a/watcher.go +++ b/watcher.go @@ -15,6 +15,7 @@ import ( type FileWatcher interface { Close() error AddFiles(pattern *regexp.Regexp) error + add(path string) error Watch(jobs chan<- string, pattern *regexp.Regexp) } @@ -27,36 +28,7 @@ func (n NotifyWatcher) Close() error { } func (n NotifyWatcher) AddFiles(pattern *regexp.Regexp) error { - 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 n.watcher.Add(path) - } - } - return err - }) - - if err != nil { - return fmt.Errorf("filepath.Walk(): %v", err) - } - - if err := n.watcher.Add(flagDirectory); err != nil { - return fmt.Errorf("NotifyWatcher.Add(): %v", err) - } - } else { - if err := n.watcher.Add(flagDirectory); err != nil { - return fmt.Errorf("NotifyWatcher.Add(): %v", err) - } - } - } - return nil + return addFiles(n) } func (n NotifyWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { @@ -90,6 +62,10 @@ func (n NotifyWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { } } +func (n NotifyWatcher) add(path string) error { + return n.watcher.Add(path) +} + type PollingWatcher struct { watcher *pollingWatcher.Watcher } @@ -102,36 +78,7 @@ func (p PollingWatcher) Close() error { func (p PollingWatcher) AddFiles(pattern *regexp.Regexp) error { p.watcher.AddFilterHook(pollingWatcher.RegexFilterHook(pattern, false)) - 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 p.watcher.Add(path) - } - } - return err - }) - - if err != nil { - return fmt.Errorf("filepath.Walk(): %v", err) - } - - if err := p.watcher.Add(flagDirectory); err != nil { - return fmt.Errorf("PollingWatcher.Add(): %v", err) - } - } else { - if err := p.watcher.Add(flagDirectory); err != nil { - return fmt.Errorf("PollingWatcher.AddFiles(): %v", err) - } - } - } - return nil + return addFiles(p) } func (p PollingWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { @@ -169,6 +116,10 @@ func (p PollingWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { } } +func (p PollingWatcher) add(path string) error { + return p.watcher.Add(path) +} + func NewWatcher(usePolling bool) (FileWatcher, error) { if usePolling { w := pollingWatcher.New() @@ -185,3 +136,36 @@ func NewWatcher(usePolling bool) (FileWatcher, error) { }, nil } } + +func addFiles(fw FileWatcher) error { + 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 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 +} From 68bbabc424da3449681bf1c68c7e33b65ecb5776 Mon Sep 17 00:00:00 2001 From: Lambert Date: Fri, 27 Nov 2020 14:26:16 +0900 Subject: [PATCH 5/9] change module name --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c71a84b..fbdb0ff 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/daangn/CompileDaemon +module github.com/githubnemo/CompileDaemon go 1.11 From 18e1b101078f9b1888f2729f02f6e7553657b838 Mon Sep 17 00:00:00 2001 From: Sungyun Hur Date: Sun, 29 Nov 2020 16:53:43 +0900 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: githubnemo --- daemon.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/daemon.go b/daemon.go index 86356ce..7d0ffe2 100644 --- a/daemon.go +++ b/daemon.go @@ -47,7 +47,7 @@ There are command line options. -pattern=XXX – Include files whose path matches regexp XXX FILE WATCH - -polling - Which method to detect file changes. default is false + -polling - Use polling instead of FS notifications to detect changes. Default is false MISC -color - Enable colorized output @@ -391,7 +391,6 @@ func main() { log.Fatal("Graceful termination is not supported on your platform.") } - /////////////////////////////// watcher, err := NewWatcher(*flagPolling) @@ -403,7 +402,6 @@ func main() { pattern := regexp.MustCompile(*flagPattern) - // add files err = watcher.AddFiles(pattern) if err != nil { log.Fatal("watcher.Addfiles():", err) From 97ffc98b984ffa5ee7bbeacc7b8779b456c9432e Mon Sep 17 00:00:00 2001 From: Lambert Date: Sun, 29 Nov 2020 17:33:50 +0900 Subject: [PATCH 7/9] refactor global variables --- daemon.go | 19 ++++++++---- watcher.go | 84 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 69 insertions(+), 34 deletions(-) diff --git a/daemon.go b/daemon.go index 7d0ffe2..adc9e3c 100644 --- a/daemon.go +++ b/daemon.go @@ -391,8 +391,19 @@ func main() { log.Fatal("Graceful termination is not supported on your platform.") } + pattern := regexp.MustCompile(*flagPattern) - watcher, err := NewWatcher(*flagPolling) + cfg := &WatcherConfig{ + flagVerbose: *flagVerbose, + flagRecursive: *flagRecursive, + flagPolling: *flagPolling, + flagDirectories: flagDirectories, + flagExcludedDirs: flagExcludedDirs, + flagExcludedFiles: flagExcludedFiles, + flagIncludedFiles: flagIncludedFiles, + pattern: pattern, + } + watcher, err := NewWatcher(cfg) if err != nil { log.Fatal(err) @@ -400,9 +411,7 @@ func main() { defer watcher.Close() - pattern := regexp.MustCompile(*flagPattern) - - err = watcher.AddFiles(pattern) + err = watcher.AddFiles() if err != nil { log.Fatal("watcher.Addfiles():", err) } @@ -419,5 +428,5 @@ func main() { go flusher(buildStarted, buildSuccess) } - watcher.Watch(jobs, pattern) // start watching files + watcher.Watch(jobs) // start watching files } diff --git a/watcher.go b/watcher.go index 33b8353..4671773 100644 --- a/watcher.go +++ b/watcher.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "github.com/fsnotify/fsnotify" pollingWatcher "github.com/radovskyb/watcher" @@ -12,41 +13,55 @@ import ( "time" ) +func pathMatches(cfg *WatcherConfig, pathName string) bool { + base := filepath.Base(pathName) + return (cfg.flagIncludedFiles.Matches(base) || matchesPattern(cfg.pattern, pathName)) && + !cfg.flagExcludedFiles.Matches(base) +} + +type WatcherConfig struct { + flagVerbose bool + flagPolling bool + flagRecursive bool + flagDirectories globList + flagExcludedDirs globList + flagExcludedFiles globList + flagIncludedFiles globList + pattern *regexp.Regexp +} + type FileWatcher interface { Close() error - AddFiles(pattern *regexp.Regexp) error + AddFiles() error add(path string) error - Watch(jobs chan<- string, pattern *regexp.Regexp) + 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(pattern *regexp.Regexp) error { +func (n NotifyWatcher) AddFiles() error { return addFiles(n) } -func (n NotifyWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { +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 { - base := filepath.Base(ev.Name) - // Assume it is a directory and track it. - if *flagRecursive == true && !flagExcludedDirs.Matches(ev.Name) { + if n.cfg.flagRecursive == true && !n.cfg.flagExcludedDirs.Matches(ev.Name) { n.watcher.Add(ev.Name) } - - if flagIncludedFiles.Matches(base) || matchesPattern(pattern, ev.Name) { - if !flagExcludedFiles.Matches(base) { - jobs <- ev.Name - } + if pathMatches(n.cfg, ev.Name) { + jobs <- ev.Name } } @@ -66,8 +81,13 @@ 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 { @@ -75,13 +95,13 @@ func (p PollingWatcher) Close() error { return nil } -func (p PollingWatcher) AddFiles(pattern *regexp.Regexp) error { - p.watcher.AddFilterHook(pollingWatcher.RegexFilterHook(pattern, false)) +func (p PollingWatcher) AddFiles() error { + p.watcher.AddFilterHook(pollingWatcher.RegexFilterHook(p.cfg.pattern, false)) return addFiles(p) } -func (p PollingWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { +func (p PollingWatcher) Watch(jobs chan<- string) { // Start the watching process. go func() { if err := p.watcher.Start(PollingInterval * time.Millisecond); err != nil { @@ -92,21 +112,16 @@ func (p PollingWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) { for { select { case event := <-p.watcher.Event: - if *flagVerbose { + if p.cfg.flagVerbose { // Print the event's info. fmt.Println(event) } - base := filepath.Base(event.Path) - - if flagIncludedFiles.Matches(base) || matchesPattern(pattern, event.Path) { - if !flagExcludedFiles.Matches(base) { - jobs <- event.String() - } + if pathMatches(p.cfg, event.Path) { + jobs <- event.String() } case err := <-p.watcher.Error: if err == pollingWatcher.ErrWatchedFileDeleted { - fmt.Println(err) continue } log.Fatalln(err) @@ -120,11 +135,20 @@ func (p PollingWatcher) add(path string) error { return p.watcher.Add(path) } -func NewWatcher(usePolling bool) (FileWatcher, error) { - if usePolling { +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() @@ -133,19 +157,21 @@ func NewWatcher(usePolling bool) (FileWatcher, error) { } return NotifyWatcher{ watcher: w, + cfg: cfg, }, nil } } func addFiles(fw FileWatcher) error { - for _, flagDirectory := range flagDirectories { - if *flagRecursive == true { + 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 flagExcludedDirs.Matches(path) { + if cfg.flagExcludedDirs.Matches(path) { return filepath.SkipDir } else { - if *flagVerbose { + if cfg.flagVerbose { log.Printf("Watching directory '%s' for changes.\n", path) } return fw.add(path) From fb7fce419e0a091d4c9673637423a98f00a09d66 Mon Sep 17 00:00:00 2001 From: Lambert Date: Sun, 29 Nov 2020 17:54:55 +0900 Subject: [PATCH 8/9] make polling interval a parameter --- daemon.go | 21 ++++++++++----------- watcher.go | 31 ++++++++++++++++++------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/daemon.go b/daemon.go index adc9e3c..748421c 100644 --- a/daemon.go +++ b/daemon.go @@ -84,9 +84,6 @@ import ( // Milliseconds to wait for the next job to begin after a file change const WorkDelay = 900 -// Milliseconds of interval between polling file changes when polling option is selected -const PollingInterval = 100 - // Default pattern to match files which trigger a build const FilePattern = `(.+\.go|.+\.c)$` @@ -124,6 +121,7 @@ var ( 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 @@ -394,14 +392,15 @@ func main() { pattern := regexp.MustCompile(*flagPattern) cfg := &WatcherConfig{ - flagVerbose: *flagVerbose, - flagRecursive: *flagRecursive, - flagPolling: *flagPolling, - flagDirectories: flagDirectories, - flagExcludedDirs: flagExcludedDirs, - flagExcludedFiles: flagExcludedFiles, - flagIncludedFiles: flagIncludedFiles, - pattern: pattern, + flagVerbose: *flagVerbose, + flagRecursive: *flagRecursive, + flagPolling: *flagPolling, + flagPollingInterval: *flagPollingInterval, + flagDirectories: flagDirectories, + flagExcludedDirs: flagExcludedDirs, + flagExcludedFiles: flagExcludedFiles, + flagIncludedFiles: flagIncludedFiles, + pattern: pattern, } watcher, err := NewWatcher(cfg) diff --git a/watcher.go b/watcher.go index 4671773..35944d1 100644 --- a/watcher.go +++ b/watcher.go @@ -13,21 +13,26 @@ import ( "time" ) -func pathMatches(cfg *WatcherConfig, pathName string) bool { - base := filepath.Base(pathName) - return (cfg.flagIncludedFiles.Matches(base) || matchesPattern(cfg.pattern, pathName)) && +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 - flagDirectories globList - flagExcludedDirs globList - flagExcludedFiles globList - flagIncludedFiles globList - pattern *regexp.Regexp + flagVerbose bool + flagPolling bool + flagRecursive bool + flagPollingInterval int + flagDirectories globList + flagExcludedDirs globList + flagExcludedFiles globList + flagIncludedFiles globList + pattern *regexp.Regexp } type FileWatcher interface { @@ -57,7 +62,7 @@ func (n NotifyWatcher) Watch(jobs chan<- string) { 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 n.cfg.flagRecursive == true && !n.cfg.flagExcludedDirs.Matches(ev.Name) { + if directoryShouldBeTracked(n.cfg, ev.Name) { n.watcher.Add(ev.Name) } if pathMatches(n.cfg, ev.Name) { @@ -104,7 +109,7 @@ func (p PollingWatcher) AddFiles() error { func (p PollingWatcher) Watch(jobs chan<- string) { // Start the watching process. go func() { - if err := p.watcher.Start(PollingInterval * time.Millisecond); err != nil { + if err := p.watcher.Start(time.Duration(p.cfg.flagPollingInterval) * time.Millisecond); err != nil { log.Fatalln(err) } }() From 36b48cd5b5bf00405a17870e303069f8780e89ee Mon Sep 17 00:00:00 2001 From: Lambert Date: Sun, 29 Nov 2020 17:57:38 +0900 Subject: [PATCH 9/9] update README.md --- README.md | 3 ++- daemon.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d40609..f2bfeef 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ the env var `GO111MODULE=on`, which enables you to develop outside of |`-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 | Which method to detect file changes. +|`-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 748421c..f83f7ac 100644 --- a/daemon.go +++ b/daemon.go @@ -48,6 +48,7 @@ There are command line options. 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