Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
75 changes: 16 additions & 59 deletions daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

// 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)$`

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -386,45 +391,24 @@ 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)
}

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)
Expand All @@ -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
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
171 changes: 171 additions & 0 deletions watcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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
add(path string) 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 {
return addFiles(n)
}

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)
}
}
}

func (n NotifyWatcher) add(path string) error {
return n.watcher.Add(path)
}

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))

return addFiles(p)
}

func (p PollingWatcher) Watch(jobs chan<- string, pattern *regexp.Regexp) {
// Start the watching process.
go func() {
if err := p.watcher.Start(PollingInterval * time.Millisecond); 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 (p PollingWatcher) add(path string) error {
return p.watcher.Add(path)
}

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
}
}

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
}