github.com/mlmmr/revel-cmd@v0.21.2-0.20191112133115-68d8795776dd/watcher/watcher.go (about)

     1  // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
     2  // Revel Framework source code and usage is governed by a MIT style
     3  // license that can be found in the LICENSE file.
     4  
     5  package watcher
     6  
     7  import (
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/mlmmr/revel-cmd/model"
    14  	"github.com/mlmmr/revel-cmd/utils"
    15  	"gopkg.in/fsnotify/fsnotify.v1"
    16  	"time"
    17  )
    18  
    19  // Listener is an interface for receivers of filesystem events.
    20  type Listener interface {
    21  	// Refresh is invoked by the watcher on relevant filesystem events.
    22  	// If the listener returns an error, it is served to the user on the current request.
    23  	Refresh() *utils.Error
    24  }
    25  
    26  // DiscerningListener allows the receiver to selectively watch files.
    27  type DiscerningListener interface {
    28  	Listener
    29  	WatchDir(info os.FileInfo) bool
    30  	WatchFile(basename string) bool
    31  }
    32  
    33  // Watcher allows listeners to register to be notified of changes under a given
    34  // directory.
    35  type Watcher struct {
    36  	// Parallel arrays of watcher/listener pairs.
    37  	watchers            []*fsnotify.Watcher
    38  	listeners           []Listener
    39  	forceRefresh        bool
    40  	eagerRefresh        bool
    41  	serial              bool
    42  	lastError           int
    43  	notifyMutex         sync.Mutex
    44  	paths               *model.RevelContainer
    45  	refreshTimer        *time.Timer // The timer to countdown the next refresh
    46  	timerMutex          *sync.Mutex // A mutex to prevent concurrent updates
    47  	refreshChannel      chan *utils.Error
    48  	refreshChannelCount int
    49  	refreshTimerMS      time.Duration // The number of milliseconds between refreshing builds
    50  }
    51  
    52  // Creates a new watched based on the container
    53  func NewWatcher(paths *model.RevelContainer, eagerRefresh bool) *Watcher {
    54  	return &Watcher{
    55  		forceRefresh:   true,
    56  		lastError:      -1,
    57  		paths:          paths,
    58  		refreshTimerMS: time.Duration(paths.Config.IntDefault("watch.rebuild.delay", 10)),
    59  		eagerRefresh: eagerRefresh ||
    60  			paths.DevMode &&
    61  				paths.Config.BoolDefault("watch", true) &&
    62  				paths.Config.StringDefault("watch.mode", "normal") == "eager",
    63  		timerMutex:          &sync.Mutex{},
    64  		refreshChannel:      make(chan *utils.Error, 10),
    65  		refreshChannelCount: 0,
    66  	}
    67  }
    68  
    69  // Listen registers for events within the given root directories (recursively).
    70  func (w *Watcher) Listen(listener Listener, roots ...string) {
    71  	watcher, err := fsnotify.NewWatcher()
    72  	if err != nil {
    73  		utils.Logger.Fatal("Watcher: Failed to create watcher", "error", err)
    74  	}
    75  
    76  	// Replace the unbuffered Event channel with a buffered one.
    77  	// Otherwise multiple change events only come out one at a time, across
    78  	// multiple page views.  (There appears no way to "pump" the events out of
    79  	// the watcher)
    80  	// This causes a notification when you do a check in go, since you are modifying a buffer in use
    81  	watcher.Events = make(chan fsnotify.Event, 100)
    82  	watcher.Errors = make(chan error, 10)
    83  
    84  	// Walk through all files / directories under the root, adding each to watcher.
    85  	for _, p := range roots {
    86  		// is the directory / file a symlink?
    87  		f, err := os.Lstat(p)
    88  		if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
    89  			var realPath string
    90  			realPath, err = filepath.EvalSymlinks(p)
    91  			if err != nil {
    92  				panic(err)
    93  			}
    94  			p = realPath
    95  		}
    96  
    97  		fi, err := os.Stat(p)
    98  		if err != nil {
    99  			utils.Logger.Fatal("Watcher: Failed to stat watched path", "path", p, "error", err)
   100  			continue
   101  		}
   102  
   103  		// If it is a file, watch that specific file.
   104  		if !fi.IsDir() {
   105  			err = watcher.Add(p)
   106  			if err != nil {
   107  				utils.Logger.Fatal("Watcher: Failed to watch", "path", p, "error", err)
   108  			}
   109  			continue
   110  		}
   111  
   112  		var watcherWalker func(path string, info os.FileInfo, err error) error
   113  
   114  		watcherWalker = func(path string, info os.FileInfo, err error) error {
   115  			if err != nil {
   116  				utils.Logger.Fatal("Watcher: Error walking path:", "error", err)
   117  				return nil
   118  			}
   119  
   120  			if info.IsDir() {
   121  				if dl, ok := listener.(DiscerningListener); ok {
   122  					if !dl.WatchDir(info) {
   123  						return filepath.SkipDir
   124  					}
   125  				}
   126  
   127  				err = watcher.Add(path)
   128  				if err != nil {
   129  					utils.Logger.Fatal("Watcher: Failed to watch", "path", path, "error", err)
   130  				}
   131  			}
   132  			return nil
   133  		}
   134  
   135  		// Else, walk the directory tree.
   136  		err = utils.Walk(p, watcherWalker)
   137  		if err != nil {
   138  			utils.Logger.Fatal("Watcher: Failed to walk directory", "path", p, "error", err)
   139  		}
   140  	}
   141  
   142  	if w.eagerRefresh {
   143  		// Create goroutine to notify file changes in real time
   144  		go w.NotifyWhenUpdated(listener, watcher)
   145  	}
   146  
   147  	w.watchers = append(w.watchers, watcher)
   148  	w.listeners = append(w.listeners, listener)
   149  }
   150  
   151  // NotifyWhenUpdated notifies the watcher when a file event is received.
   152  func (w *Watcher) NotifyWhenUpdated(listener Listener, watcher *fsnotify.Watcher) {
   153  
   154  	for {
   155  		select {
   156  		case ev := <-watcher.Events:
   157  			if w.rebuildRequired(ev, listener) {
   158  				if w.serial {
   159  					// Serialize listener.Refresh() calls.
   160  					w.notifyMutex.Lock()
   161  
   162  					if err := listener.Refresh(); err != nil {
   163  						utils.Logger.Error("Watcher: Listener refresh reported error:", "error", err)
   164  					}
   165  					w.notifyMutex.Unlock()
   166  				} else {
   167  					// Run refresh in parallel
   168  					go func() {
   169  						w.notifyInProcess(listener)
   170  					}()
   171  				}
   172  			}
   173  		case <-watcher.Errors:
   174  			continue
   175  		}
   176  	}
   177  }
   178  
   179  // Notify causes the watcher to forward any change events to listeners.
   180  // It returns the first (if any) error returned.
   181  func (w *Watcher) Notify() *utils.Error {
   182  	if w.serial {
   183  		// Serialize Notify() calls.
   184  		w.notifyMutex.Lock()
   185  		defer w.notifyMutex.Unlock()
   186  	}
   187  
   188  	for i, watcher := range w.watchers {
   189  		listener := w.listeners[i]
   190  
   191  		// Pull all pending events / errors from the watcher.
   192  		refresh := false
   193  		for {
   194  			select {
   195  			case ev := <-watcher.Events:
   196  				if w.rebuildRequired(ev, listener) {
   197  					refresh = true
   198  				}
   199  				continue
   200  			case <-watcher.Errors:
   201  				continue
   202  			default:
   203  				// No events left to pull
   204  			}
   205  			break
   206  		}
   207  
   208  		utils.Logger.Info("Watcher:Notify refresh state", "Current Index", i, " last error index", w.lastError)
   209  		if w.forceRefresh || refresh || w.lastError == i {
   210  			var err *utils.Error
   211  			if w.serial {
   212  				err = listener.Refresh()
   213  			} else {
   214  				err = w.notifyInProcess(listener)
   215  			}
   216  			if err != nil {
   217  				w.lastError = i
   218  				w.forceRefresh = true
   219  				return err
   220  			} else {
   221  				w.lastError = -1
   222  				w.forceRefresh = false
   223  			}
   224  		}
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  // Build a queue for refresh notifications
   231  // this will not return until one of the queue completes
   232  func (w *Watcher) notifyInProcess(listener Listener) (err *utils.Error) {
   233  	shouldReturn := false
   234  	// This code block ensures that either a timer is created
   235  	// or that a process would be added the the h.refreshChannel
   236  	func() {
   237  		w.timerMutex.Lock()
   238  		defer w.timerMutex.Unlock()
   239  		// If we are in the process of a rebuild, forceRefresh will always be true
   240  		w.forceRefresh = true
   241  		if w.refreshTimer != nil {
   242  			utils.Logger.Info("Found existing timer running, resetting")
   243  			w.refreshTimer.Reset(time.Millisecond * w.refreshTimerMS)
   244  			shouldReturn = true
   245  			w.refreshChannelCount++
   246  		} else {
   247  			w.refreshTimer = time.NewTimer(time.Millisecond * w.refreshTimerMS)
   248  		}
   249  	}()
   250  
   251  	// If another process is already waiting for the timer this one
   252  	// only needs to return the output from the channel
   253  	if shouldReturn {
   254  		return <-w.refreshChannel
   255  	}
   256  	utils.Logger.Info("Waiting for refresh timer to expire")
   257  	<-w.refreshTimer.C
   258  	w.timerMutex.Lock()
   259  
   260  	// Ensure the queue is properly dispatched even if a panic occurs
   261  	defer func() {
   262  		for x := 0; x < w.refreshChannelCount; x++ {
   263  			w.refreshChannel <- err
   264  		}
   265  		w.refreshChannelCount = 0
   266  		w.refreshTimer = nil
   267  		w.timerMutex.Unlock()
   268  	}()
   269  
   270  	err = listener.Refresh()
   271  	if err != nil {
   272  		utils.Logger.Info("Watcher: Recording error last build, setting rebuild on", "error", err)
   273  	} else {
   274  		w.lastError = -1
   275  		w.forceRefresh = false
   276  	}
   277  	utils.Logger.Info("Rebuilt, result", "error", err)
   278  	return
   279  }
   280  
   281  func (w *Watcher) rebuildRequired(ev fsnotify.Event, listener Listener) bool {
   282  	// Ignore changes to dotfiles.
   283  	if strings.HasPrefix(filepath.Base(ev.Name), ".") {
   284  		return false
   285  	}
   286  
   287  	if dl, ok := listener.(DiscerningListener); ok {
   288  		if !dl.WatchFile(ev.Name) || ev.Op&fsnotify.Chmod == fsnotify.Chmod {
   289  			return false
   290  		}
   291  	}
   292  	return true
   293  }
   294  
   295  /*
   296  var WatchFilter = func(c *Controller, fc []Filter) {
   297  	if MainWatcher != nil {
   298  		err := MainWatcher.Notify()
   299  		if err != nil {
   300  			c.Result = c.RenderError(err)
   301  			return
   302  		}
   303  	}
   304  	fc[0](c, fc[1:])
   305  }
   306  */