mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
Add polling as a fallback to native filesystem events in server watch
Fixes #8720 Fixes #6849 Fixes #7930
This commit is contained in:
parent
0019d60f67
commit
24ce98b6d1
8 changed files with 731 additions and 13 deletions
|
@ -204,6 +204,7 @@ type hugoBuilderCommon struct {
|
||||||
environment string
|
environment string
|
||||||
|
|
||||||
buildWatch bool
|
buildWatch bool
|
||||||
|
poll bool
|
||||||
|
|
||||||
gc bool
|
gc bool
|
||||||
|
|
||||||
|
@ -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().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().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.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("templateMetrics", false, "display metrics about template executions")
|
||||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||||
|
|
|
@ -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.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
|
||||||
c.logger.Println("Press Ctrl+C to stop")
|
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)
|
checkErr(c.Logger, err)
|
||||||
defer watcher.Close()
|
defer watcher.Close()
|
||||||
|
|
||||||
|
@ -820,7 +820,7 @@ func (c *commandeer) fullRebuild(changeType string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWatcher creates a new watcher to watch filesystem events.
|
// 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" {
|
if runtime.GOOS == "darwin" {
|
||||||
tweakLimit()
|
tweakLimit()
|
||||||
}
|
}
|
||||||
|
@ -830,7 +830,10 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
watcher, err := watcher.New(1 * time.Second)
|
// The second interval is used by the poll based watcher.
|
||||||
|
// Setting a shorter interval would make it snappier,
|
||||||
|
// but it would consume more CPU.
|
||||||
|
watcher, err := watcher.New(500*time.Millisecond, 700*time.Millisecond, poll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -859,7 +862,7 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||||
// Need to reload browser to show the error
|
// Need to reload browser to show the error
|
||||||
livereload.ForceRefresh()
|
livereload.ForceRefresh()
|
||||||
}
|
}
|
||||||
case err := <-watcher.Errors:
|
case err := <-watcher.Errors():
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Errorln("Error while watching:", err)
|
c.logger.Errorln("Error while watching:", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,7 +262,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||||
for _, group := range watchGroups {
|
for _, group := range watchGroups {
|
||||||
jww.FEEDBACK.Printf("Watching for changes in %s\n", group)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/gohugoio/hugo/watcher/filenotify"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Batcher batches file watch events in a given interval.
|
// Batcher batches file watch events in a given interval.
|
||||||
type Batcher struct {
|
type Batcher struct {
|
||||||
*fsnotify.Watcher
|
filenotify.FileWatcher
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
|
||||||
|
@ -29,12 +30,25 @@ type Batcher struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and starts a Batcher with the given time interval.
|
// New creates and starts a Batcher with the given time interval.
|
||||||
func New(interval time.Duration) (*Batcher, error) {
|
// It will fall back to a poll based watcher if native isn's supported.
|
||||||
watcher, err := fsnotify.NewWatcher()
|
// To always use polling, set poll to true.
|
||||||
|
func New(intervalBatcher, intervalPoll time.Duration, poll bool) (*Batcher, error) {
|
||||||
|
var err error
|
||||||
|
var watcher filenotify.FileWatcher
|
||||||
|
|
||||||
|
if poll {
|
||||||
|
watcher = filenotify.NewPollingWatcher(intervalPoll)
|
||||||
|
} else {
|
||||||
|
watcher, err = filenotify.New(intervalPoll)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
batcher := &Batcher{}
|
batcher := &Batcher{}
|
||||||
batcher.Watcher = watcher
|
batcher.FileWatcher = watcher
|
||||||
batcher.interval = interval
|
batcher.interval = intervalBatcher
|
||||||
batcher.done = make(chan struct{}, 1)
|
batcher.done = make(chan struct{}, 1)
|
||||||
batcher.Events = make(chan []fsnotify.Event, 1)
|
batcher.Events = make(chan []fsnotify.Event, 1)
|
||||||
|
|
||||||
|
@ -42,7 +56,7 @@ func New(interval time.Duration) (*Batcher, error) {
|
||||||
go batcher.run()
|
go batcher.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
return batcher, err
|
return batcher, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Batcher) run() {
|
func (b *Batcher) run() {
|
||||||
|
@ -51,7 +65,7 @@ func (b *Batcher) run() {
|
||||||
OuterLoop:
|
OuterLoop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case ev := <-b.Watcher.Events:
|
case ev := <-b.FileWatcher.Events():
|
||||||
evs = append(evs, ev)
|
evs = append(evs, ev)
|
||||||
case <-tick:
|
case <-tick:
|
||||||
if len(evs) == 0 {
|
if len(evs) == 0 {
|
||||||
|
@ -69,5 +83,5 @@ OuterLoop:
|
||||||
// Close stops the watching of the files.
|
// Close stops the watching of the files.
|
||||||
func (b *Batcher) Close() {
|
func (b *Batcher) Close() {
|
||||||
b.done <- struct{}{}
|
b.done <- struct{}{}
|
||||||
b.Watcher.Close()
|
b.FileWatcher.Close()
|
||||||
}
|
}
|
||||||
|
|
49
watcher/filenotify/filenotify.go
Normal file
49
watcher/filenotify/filenotify.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// 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 (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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(interval time.Duration) (FileWatcher, error) {
|
||||||
|
if watcher, err := NewEventWatcher(); err == nil {
|
||||||
|
return watcher, nil
|
||||||
|
}
|
||||||
|
return NewPollingWatcher(interval), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPollingWatcher returns a poll-based file watcher
|
||||||
|
func NewPollingWatcher(interval time.Duration) FileWatcher {
|
||||||
|
return &filePoller{
|
||||||
|
interval: interval,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
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
watcher/filenotify/fsnotify.go
Normal file
20
watcher/filenotify/fsnotify.go
Normal file
|
@ -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
|
||||||
|
}
|
326
watcher/filenotify/poller.go
Normal file
326
watcher/filenotify/poller.go
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// errPollerClosed is returned when the poller is closed
|
||||||
|
errPollerClosed = errors.New("poller is closed")
|
||||||
|
// errNoSuchWatch is returned when trying to remove a watch that doesn't exist
|
||||||
|
errNoSuchWatch = errors.New("watch does not exist")
|
||||||
|
)
|
||||||
|
|
||||||
|
// filePoller is used to poll files for changes, especially in cases where fsnotify
|
||||||
|
// can't be run (e.g. when inotify handles are exhausted)
|
||||||
|
// filePoller satisfies the FileWatcher interface
|
||||||
|
type filePoller struct {
|
||||||
|
// the duration between polls.
|
||||||
|
interval time.Duration
|
||||||
|
// watches is the list of files currently being polled, close the associated channel to stop the watch
|
||||||
|
watches map[string]struct{}
|
||||||
|
// Will be closed when done.
|
||||||
|
done chan struct{}
|
||||||
|
// events is the channel to listen to for watch events
|
||||||
|
events chan fsnotify.Event
|
||||||
|
// errors is the channel to listen to for watch errors
|
||||||
|
errors chan error
|
||||||
|
// mu locks the poller for modification
|
||||||
|
mu sync.Mutex
|
||||||
|
// closed is used to specify when the poller has already closed
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a filename to the list of watches
|
||||||
|
// once added the file is polled for changes in a separate goroutine
|
||||||
|
func (w *filePoller) Add(name string) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.closed {
|
||||||
|
return errPollerClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := newItemToWatch(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if item.left.FileInfo == nil {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.watches == nil {
|
||||||
|
w.watches = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
if _, exists := w.watches[name]; exists {
|
||||||
|
return fmt.Errorf("watch exists")
|
||||||
|
}
|
||||||
|
w.watches[name] = struct{}{}
|
||||||
|
|
||||||
|
go w.watch(item)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stops and removes watch with the specified name
|
||||||
|
func (w *filePoller) Remove(name string) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
return w.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *filePoller) remove(name string) error {
|
||||||
|
if w.closed {
|
||||||
|
return errPollerClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
_, exists := w.watches[name]
|
||||||
|
if !exists {
|
||||||
|
return errNoSuchWatch
|
||||||
|
}
|
||||||
|
delete(w.watches, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events returns the event channel
|
||||||
|
// This is used for notifications on events about watched files
|
||||||
|
func (w *filePoller) Events() <-chan fsnotify.Event {
|
||||||
|
return w.events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors returns the errors channel
|
||||||
|
// This is used for notifications about errors on watched files
|
||||||
|
func (w *filePoller) Errors() <-chan error {
|
||||||
|
return w.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the poller
|
||||||
|
// All watches are stopped, removed, and the poller cannot be added to
|
||||||
|
func (w *filePoller) Close() error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.closed = true
|
||||||
|
close(w.done)
|
||||||
|
for name := range w.watches {
|
||||||
|
w.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEvent publishes the specified event to the events channel
|
||||||
|
func (w *filePoller) sendEvent(e fsnotify.Event) error {
|
||||||
|
select {
|
||||||
|
case w.events <- e:
|
||||||
|
case <-w.done:
|
||||||
|
return fmt.Errorf("closed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendErr publishes the specified error to the errors channel
|
||||||
|
func (w *filePoller) sendErr(e error) error {
|
||||||
|
select {
|
||||||
|
case w.errors <- e:
|
||||||
|
case <-w.done:
|
||||||
|
return fmt.Errorf("closed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch watches item for changes until done is closed.
|
||||||
|
func (w *filePoller) watch(item *itemToWatch) {
|
||||||
|
ticker := time.NewTicker(w.interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
case <-w.done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
evs, err := item.checkForChanges()
|
||||||
|
if err != nil {
|
||||||
|
if err := w.sendErr(err); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.left, item.right = item.right, item.left
|
||||||
|
|
||||||
|
for _, ev := range evs {
|
||||||
|
if err := w.sendEvent(ev); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recording records the state of a file or a dir.
|
||||||
|
type recording struct {
|
||||||
|
os.FileInfo
|
||||||
|
|
||||||
|
// Set if FileInfo is a dir.
|
||||||
|
entries map[string]os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *recording) clear() {
|
||||||
|
r.FileInfo = nil
|
||||||
|
if r.entries != nil {
|
||||||
|
for k := range r.entries {
|
||||||
|
delete(r.entries, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *recording) record(filename string) error {
|
||||||
|
r.clear()
|
||||||
|
|
||||||
|
fi, err := os.Stat(filename)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.FileInfo = fi
|
||||||
|
|
||||||
|
// If fi is a dir, we watch the files inside that directory (not recursively).
|
||||||
|
// This matches the behaviour of fsnotity.
|
||||||
|
if fi.IsDir() {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fis, err := f.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fi := range fis {
|
||||||
|
r.entries[fi.Name()] = fi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// itemToWatch may be a file or a dir.
|
||||||
|
type itemToWatch struct {
|
||||||
|
// Full path to the filename.
|
||||||
|
filename string
|
||||||
|
|
||||||
|
// Snapshots of the stat state of this file or dir.
|
||||||
|
left *recording
|
||||||
|
right *recording
|
||||||
|
}
|
||||||
|
|
||||||
|
func newItemToWatch(filename string) (*itemToWatch, error) {
|
||||||
|
r := &recording{
|
||||||
|
entries: make(map[string]os.FileInfo),
|
||||||
|
}
|
||||||
|
err := r.record(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &itemToWatch{filename: filename, left: r}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *itemToWatch) checkForChanges() ([]fsnotify.Event, error) {
|
||||||
|
if item.right == nil {
|
||||||
|
item.right = &recording{
|
||||||
|
entries: make(map[string]os.FileInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := item.right.record(item.filename)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dirOp := checkChange(item.left.FileInfo, item.right.FileInfo)
|
||||||
|
|
||||||
|
if dirOp != 0 {
|
||||||
|
evs := []fsnotify.Event{fsnotify.Event{Op: dirOp, Name: item.filename}}
|
||||||
|
return evs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.left.FileInfo == nil || !item.left.IsDir() {
|
||||||
|
// Done.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
leftIsIn := false
|
||||||
|
left, right := item.left.entries, item.right.entries
|
||||||
|
if len(right) > len(left) {
|
||||||
|
left, right = right, left
|
||||||
|
leftIsIn = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var evs []fsnotify.Event
|
||||||
|
|
||||||
|
for name, fi1 := range left {
|
||||||
|
fi2 := right[name]
|
||||||
|
fil, fir := fi1, fi2
|
||||||
|
if leftIsIn {
|
||||||
|
fil, fir = fir, fil
|
||||||
|
}
|
||||||
|
op := checkChange(fil, fir)
|
||||||
|
if op != 0 {
|
||||||
|
evs = append(evs, fsnotify.Event{Op: op, Name: filepath.Join(item.filename, name)})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return evs, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkChange(fi1, fi2 os.FileInfo) fsnotify.Op {
|
||||||
|
if fi1 == nil && fi2 != nil {
|
||||||
|
return fsnotify.Create
|
||||||
|
}
|
||||||
|
if fi1 != nil && fi2 == nil {
|
||||||
|
return fsnotify.Remove
|
||||||
|
}
|
||||||
|
if fi1 == nil && fi2 == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if fi1.IsDir() || fi2.IsDir() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if fi1.Mode() != fi2.Mode() {
|
||||||
|
return fsnotify.Chmod
|
||||||
|
}
|
||||||
|
if fi1.ModTime() != fi2.ModTime() || fi1.Size() != fi2.Size() {
|
||||||
|
return fsnotify.Write
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
304
watcher/filenotify/poller_test.go
Normal file
304
watcher/filenotify/poller_test.go
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/gohugoio/hugo/htesting"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
subdir1 = "subdir1"
|
||||||
|
subdir2 = "subdir2"
|
||||||
|
watchWaitTime = 200 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
isMacOs = runtime.GOOS == "darwin"
|
||||||
|
isWindows = runtime.GOOS == "windows"
|
||||||
|
isCI = htesting.IsCI()
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPollerAddRemove(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
w := NewPollingWatcher(watchWaitTime)
|
||||||
|
|
||||||
|
c.Assert(w.Add("foo"), qt.Not(qt.IsNil))
|
||||||
|
c.Assert(w.Remove("foo"), qt.Not(qt.IsNil))
|
||||||
|
|
||||||
|
f, err := ioutil.TempFile("", "asdf")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(f.Name())
|
||||||
|
c.Assert(w.Add(f.Name()), qt.IsNil)
|
||||||
|
c.Assert(w.Remove(f.Name()), qt.IsNil)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPollerEvent(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
|
||||||
|
for _, poll := range []bool{true, false} {
|
||||||
|
if !(poll || isMacOs) || isCI {
|
||||||
|
// Only run the fsnotify tests on MacOS locally.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
method := "fsnotify"
|
||||||
|
if poll {
|
||||||
|
method = "poll"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Run(fmt.Sprintf("%s, Watch dir", method), func(c *qt.C) {
|
||||||
|
dir, w := preparePollTest(c, poll)
|
||||||
|
subdir := filepath.Join(dir, subdir1)
|
||||||
|
c.Assert(w.Add(subdir), qt.IsNil)
|
||||||
|
|
||||||
|
filename := filepath.Join(subdir, "file1")
|
||||||
|
|
||||||
|
// Write to one file.
|
||||||
|
c.Assert(ioutil.WriteFile(filename, []byte("changed"), 0600), qt.IsNil)
|
||||||
|
|
||||||
|
var expected []fsnotify.Event
|
||||||
|
|
||||||
|
if poll {
|
||||||
|
expected = append(expected, fsnotify.Event{Name: filename, Op: fsnotify.Write})
|
||||||
|
assertEvents(c, w, expected...)
|
||||||
|
} else {
|
||||||
|
// fsnotify sometimes emits Chmod before Write,
|
||||||
|
// which is hard to test, so skip it here.
|
||||||
|
drainEvents(c, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove one file.
|
||||||
|
filename = filepath.Join(subdir, "file2")
|
||||||
|
c.Assert(os.Remove(filename), qt.IsNil)
|
||||||
|
assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Remove})
|
||||||
|
|
||||||
|
// Add one file.
|
||||||
|
filename = filepath.Join(subdir, "file3")
|
||||||
|
c.Assert(ioutil.WriteFile(filename, []byte("new"), 0600), qt.IsNil)
|
||||||
|
assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Create})
|
||||||
|
|
||||||
|
// Remove entire directory.
|
||||||
|
subdir = filepath.Join(dir, subdir2)
|
||||||
|
c.Assert(w.Add(subdir), qt.IsNil)
|
||||||
|
|
||||||
|
c.Assert(os.RemoveAll(subdir), qt.IsNil)
|
||||||
|
|
||||||
|
expected = expected[:0]
|
||||||
|
|
||||||
|
// This looks like a bug in fsnotify on MacOS. There are
|
||||||
|
// 3 files in this directory, yet we get Remove events
|
||||||
|
// for one of them + the directory.
|
||||||
|
if !poll {
|
||||||
|
expected = append(expected, fsnotify.Event{Name: filepath.Join(subdir, "file2"), Op: fsnotify.Remove})
|
||||||
|
}
|
||||||
|
expected = append(expected, fsnotify.Event{Name: subdir, Op: fsnotify.Remove})
|
||||||
|
assertEvents(c, w, expected...)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Run(fmt.Sprintf("%s, Add should not trigger event", method), func(c *qt.C) {
|
||||||
|
dir, w := preparePollTest(c, poll)
|
||||||
|
subdir := filepath.Join(dir, subdir1)
|
||||||
|
w.Add(subdir)
|
||||||
|
assertEvents(c, w)
|
||||||
|
// Create a new sub directory and add it to the watcher.
|
||||||
|
subdir = filepath.Join(dir, subdir1, subdir2)
|
||||||
|
c.Assert(os.Mkdir(subdir, 0777), qt.IsNil)
|
||||||
|
w.Add(subdir)
|
||||||
|
// This should create only one event.
|
||||||
|
assertEvents(c, w, fsnotify.Event{Name: subdir, Op: fsnotify.Create})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPollerClose(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
w := NewPollingWatcher(watchWaitTime)
|
||||||
|
f1, err := ioutil.TempFile("", "f1")
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
f2, err := ioutil.TempFile("", "f2")
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
filename1 := f1.Name()
|
||||||
|
filename2 := f2.Name()
|
||||||
|
f1.Close()
|
||||||
|
f2.Close()
|
||||||
|
|
||||||
|
c.Assert(w.Add(filename1), qt.IsNil)
|
||||||
|
c.Assert(w.Add(filename2), qt.IsNil)
|
||||||
|
c.Assert(w.Close(), qt.IsNil)
|
||||||
|
c.Assert(w.Close(), qt.IsNil)
|
||||||
|
c.Assert(ioutil.WriteFile(filename1, []byte("new"), 0600), qt.IsNil)
|
||||||
|
c.Assert(ioutil.WriteFile(filename2, []byte("new"), 0600), qt.IsNil)
|
||||||
|
// No more event as the watchers are closed.
|
||||||
|
assertEvents(c, w)
|
||||||
|
|
||||||
|
f2, err = ioutil.TempFile("", "f2")
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
defer os.Remove(f2.Name())
|
||||||
|
|
||||||
|
c.Assert(w.Add(f2.Name()), qt.Not(qt.IsNil))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckChange(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
|
||||||
|
dir := prepareTestDirWithSomeFiles(c, "check-change")
|
||||||
|
|
||||||
|
stat := func(s ...string) os.FileInfo {
|
||||||
|
fi, err := os.Stat(filepath.Join(append([]string{dir}, s...)...))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
return fi
|
||||||
|
}
|
||||||
|
|
||||||
|
f0, f1, f2 := stat(subdir2, "file0"), stat(subdir2, "file1"), stat(subdir2, "file2")
|
||||||
|
d1 := stat(subdir1)
|
||||||
|
|
||||||
|
// Note that on Windows, only the 0200 bit (owner writable) of mode is used.
|
||||||
|
c.Assert(os.Chmod(filepath.Join(filepath.Join(dir, subdir2, "file1")), 0400), qt.IsNil)
|
||||||
|
f1_2 := stat(subdir2, "file1")
|
||||||
|
|
||||||
|
c.Assert(ioutil.WriteFile(filepath.Join(filepath.Join(dir, subdir2, "file2")), []byte("changed"), 0600), qt.IsNil)
|
||||||
|
f2_2 := stat(subdir2, "file2")
|
||||||
|
|
||||||
|
c.Assert(checkChange(f0, nil), qt.Equals, fsnotify.Remove)
|
||||||
|
c.Assert(checkChange(nil, f0), qt.Equals, fsnotify.Create)
|
||||||
|
c.Assert(checkChange(f1, f1_2), qt.Equals, fsnotify.Chmod)
|
||||||
|
c.Assert(checkChange(f2, f2_2), qt.Equals, fsnotify.Write)
|
||||||
|
c.Assert(checkChange(nil, nil), qt.Equals, fsnotify.Op(0))
|
||||||
|
c.Assert(checkChange(d1, f1), qt.Equals, fsnotify.Op(0))
|
||||||
|
c.Assert(checkChange(f1, d1), qt.Equals, fsnotify.Op(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPoller(b *testing.B) {
|
||||||
|
runBench := func(b *testing.B, item *itemToWatch) {
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
evs, err := item.checkForChanges()
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(evs) != 0 {
|
||||||
|
b.Fatal("got events")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run("Check for changes in dir", func(b *testing.B) {
|
||||||
|
c := qt.New(b)
|
||||||
|
dir := prepareTestDirWithSomeFiles(c, "bench-check")
|
||||||
|
item, err := newItemToWatch(dir)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
runBench(b, item)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Check for changes in file", func(b *testing.B) {
|
||||||
|
c := qt.New(b)
|
||||||
|
dir := prepareTestDirWithSomeFiles(c, "bench-check-file")
|
||||||
|
filename := filepath.Join(dir, subdir1, "file1")
|
||||||
|
item, err := newItemToWatch(filename)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
runBench(b, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareTestDirWithSomeFiles(c *qt.C, id string) string {
|
||||||
|
dir, err := ioutil.TempDir("", fmt.Sprintf("test-poller-dir-%s", id))
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Assert(os.MkdirAll(filepath.Join(dir, subdir1), 0777), qt.IsNil)
|
||||||
|
c.Assert(os.MkdirAll(filepath.Join(dir, subdir2), 0777), qt.IsNil)
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir1, fmt.Sprintf("file%d", i)), []byte("hello1"), 0600), qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir2, fmt.Sprintf("file%d", i)), []byte("hello2"), 0600), qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cleanup(func() {
|
||||||
|
os.RemoveAll(dir)
|
||||||
|
})
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePollTest(c *qt.C, poll bool) (string, FileWatcher) {
|
||||||
|
var w FileWatcher
|
||||||
|
if poll {
|
||||||
|
w = NewPollingWatcher(watchWaitTime)
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
w, err = NewEventWatcher()
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := prepareTestDirWithSomeFiles(c, fmt.Sprint(poll))
|
||||||
|
|
||||||
|
c.Cleanup(func() {
|
||||||
|
w.Close()
|
||||||
|
})
|
||||||
|
return dir, w
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEvents(c *qt.C, w FileWatcher, evs ...fsnotify.Event) {
|
||||||
|
c.Helper()
|
||||||
|
i := 0
|
||||||
|
check := func() error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case got := <-w.Events():
|
||||||
|
if i > len(evs)-1 {
|
||||||
|
return fmt.Errorf("got too many event(s): %q", got)
|
||||||
|
}
|
||||||
|
expected := evs[i]
|
||||||
|
i++
|
||||||
|
if expected.Name != got.Name {
|
||||||
|
return fmt.Errorf("got wrong filename, expected %q: %v", expected.Name, got.Name)
|
||||||
|
} else if got.Op&expected.Op != expected.Op {
|
||||||
|
return fmt.Errorf("got wrong event type, expected %q: %v", expected.Op, got.Op)
|
||||||
|
}
|
||||||
|
case e := <-w.Errors():
|
||||||
|
return fmt.Errorf("got unexpected error waiting for events %v", e)
|
||||||
|
case <-time.After(watchWaitTime + (watchWaitTime / 2)):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Assert(check(), qt.IsNil)
|
||||||
|
c.Assert(i, qt.Equals, len(evs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func drainEvents(c *qt.C, w FileWatcher) {
|
||||||
|
c.Helper()
|
||||||
|
check := func() error {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-w.Events():
|
||||||
|
case e := <-w.Errors():
|
||||||
|
return fmt.Errorf("got unexpected error waiting for events %v", e)
|
||||||
|
case <-time.After(watchWaitTime * 2):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Assert(check(), qt.IsNil)
|
||||||
|
}
|
Loading…
Reference in a new issue