Skip to content

Commit

Permalink
Add polling as a fallback to native filesystem events in server watch
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Jul 3, 2021
1 parent b4d60b3 commit cff6de7
Show file tree
Hide file tree
Showing 8 changed files with 666 additions and 12 deletions.
2 changes: 2 additions & 0 deletions commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ type hugoBuilderCommon struct {
environment string

buildWatch bool
poll bool

gc bool

Expand Down Expand Up @@ -291,6 +292,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/")
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages")
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
cmd.Flags().BoolVar(&cc.poll, "poll", false, "use a poll based approach to watch for file system changes")

cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
Expand Down
8 changes: 4 additions & 4 deletions commands/hugo.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ func (c *commandeer) build() error {

c.logger.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
c.logger.Println("Press Ctrl+C to stop")
watcher, err := c.newWatcher(watchDirs...)
watcher, err := c.newWatcher(c.h.poll, watchDirs...)
checkErr(c.Logger, err)
defer watcher.Close()

Expand Down Expand Up @@ -820,7 +820,7 @@ func (c *commandeer) fullRebuild(changeType string) {
}

// newWatcher creates a new watcher to watch filesystem events.
func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
func (c *commandeer) newWatcher(poll bool, dirList ...string) (*watcher.Batcher, error) {
if runtime.GOOS == "darwin" {
tweakLimit()
}
Expand All @@ -830,7 +830,7 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
return nil, err
}

watcher, err := watcher.New(1 * time.Second)
watcher, err := watcher.New(1*time.Second, poll)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -859,7 +859,7 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
// Need to reload browser to show the error
livereload.ForceRefresh()
}
case err := <-watcher.Errors:
case err := <-watcher.Errors():
if err != nil {
c.logger.Errorln("Error while watching:", err)
}
Expand Down
2 changes: 1 addition & 1 deletion commands/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
for _, group := range watchGroups {
jww.FEEDBACK.Printf("Watching for changes in %s\n", group)
}
watcher, err := c.newWatcher(watchDirs...)
watcher, err := c.newWatcher(sc.poll, watchDirs...)
if err != nil {
return err
}
Expand Down
28 changes: 21 additions & 7 deletions watcher/batcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,37 @@ import (
"time"

"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/watcher/filenotify"
)

// Batcher batches file watch events in a given interval.
type Batcher struct {
*fsnotify.Watcher
filenotify.FileWatcher
interval time.Duration
done chan struct{}

Events chan []fsnotify.Event // Events are returned on this channel
}

// New creates and starts a Batcher with the given time interval.
func New(interval time.Duration) (*Batcher, error) {
watcher, err := fsnotify.NewWatcher()
// It will fall back to a poll based watcher if native isn's supported.
// To always use pollng, set poll to true.
func New(interval time.Duration, poll bool) (*Batcher, error) {
var err error
var watcher filenotify.FileWatcher

if poll {
watcher = filenotify.NewPollingWatcher()
} else {
watcher, err = filenotify.New()
}

if err != nil {
return nil, err
}

batcher := &Batcher{}
batcher.Watcher = watcher
batcher.FileWatcher = watcher
batcher.interval = interval
batcher.done = make(chan struct{}, 1)
batcher.Events = make(chan []fsnotify.Event, 1)
Expand All @@ -42,7 +56,7 @@ func New(interval time.Duration) (*Batcher, error) {
go batcher.run()
}

return batcher, err
return batcher, nil
}

func (b *Batcher) run() {
Expand All @@ -51,7 +65,7 @@ func (b *Batcher) run() {
OuterLoop:
for {
select {
case ev := <-b.Watcher.Events:
case ev := <-b.FileWatcher.Events():
evs = append(evs, ev)
case <-tick:
if len(evs) == 0 {
Expand All @@ -69,5 +83,5 @@ OuterLoop:
// Close stops the watching of the files.
func (b *Batcher) Close() {
b.done <- struct{}{}
b.Watcher.Close()
b.FileWatcher.Close()
}
43 changes: 43 additions & 0 deletions watcher/filenotify/filenotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Package filenotify provides a mechanism for watching file(s) for changes.
// Generally leans on fsnotify, but provides a poll-based notifier which fsnotify does not support.
// These are wrapped up in a common interface so that either can be used interchangeably in your code.
//
// This package is adapted from https://github.com/moby/moby/tree/master/pkg/filenotify, Apache-2.0 License.
// Hopefully this can be replaced with an external package sometime in the future, see https://github.com/fsnotify/fsnotify/issues/9
package filenotify

import "github.com/fsnotify/fsnotify"

// FileWatcher is an interface for implementing file notification watchers
type FileWatcher interface {
Events() <-chan fsnotify.Event
Errors() <-chan error
Add(name string) error
Remove(name string) error
Close() error
}

// New tries to use an fs-event watcher, and falls back to the poller if there is an error
func New() (FileWatcher, error) {
if watcher, err := NewEventWatcher(); err == nil {
return watcher, nil
}
return NewPollingWatcher(), nil
}

// NewPollingWatcher returns a poll-based file watcher
func NewPollingWatcher() FileWatcher {
return &filePoller{
events: make(chan fsnotify.Event),
errors: make(chan error),
}
}

// NewEventWatcher returns an fs-event based file watcher
func NewEventWatcher() (FileWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
return &fsNotifyWatcher{watcher}, nil
}
20 changes: 20 additions & 0 deletions watcher/filenotify/fsnotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Package filenotify is adapted from https://github.com/moby/moby/tree/master/pkg/filenotify, Apache-2.0 License.
// Hopefully this can be replaced with an external package sometime in the future, see https://github.com/fsnotify/fsnotify/issues/9
package filenotify

import "github.com/fsnotify/fsnotify"

// fsNotifyWatcher wraps the fsnotify package to satisfy the FileNotifier interface
type fsNotifyWatcher struct {
*fsnotify.Watcher
}

// Events returns the fsnotify event channel receiver
func (w *fsNotifyWatcher) Events() <-chan fsnotify.Event {
return w.Watcher.Events
}

// Errors returns the fsnotify error channel receiver
func (w *fsNotifyWatcher) Errors() <-chan error {
return w.Watcher.Errors
}
Loading

0 comments on commit cff6de7

Please sign in to comment.