github.com/wiselike/revel-cmd@v1.2.1/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  	"time"
    13  
    14  	"github.com/fsnotify/fsnotify"
    15  	"github.com/wiselike/revel-cmd/model"
    16  	"github.com/wiselike/revel-cmd/utils"
    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.SourceError
    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.SourceError
    48  	refreshChannelCount int
    49  	refreshInterval     time.Duration // The interval 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  		refreshInterval: time.Duration(paths.Config.IntDefault("watch.rebuild.delay", 1000)) * time.Millisecond,
    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.SourceError, 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  		watcherWalker := func(path string, info os.FileInfo, err error) error {
   113  			if err != nil {
   114  				utils.Logger.Fatal("Watcher: Error walking path:", "error", err)
   115  				return nil
   116  			}
   117  
   118  			if info.IsDir() {
   119  				if dl, ok := listener.(DiscerningListener); ok {
   120  					if !dl.WatchDir(info) {
   121  						return filepath.SkipDir
   122  					}
   123  				}
   124  
   125  				err = watcher.Add(path)
   126  				if err != nil {
   127  					utils.Logger.Fatal("Watcher: Failed to watch", "path", path, "error", err)
   128  				}
   129  			}
   130  			return nil
   131  		}
   132  
   133  		// Else, walk the directory tree.
   134  		err = utils.Walk(p, watcherWalker)
   135  		if err != nil {
   136  			utils.Logger.Fatal("Watcher: Failed to walk directory", "path", p, "error", err)
   137  		}
   138  	}
   139  
   140  	if w.eagerRefresh {
   141  		// Create goroutine to notify file changes in real time
   142  		go w.NotifyWhenUpdated(listener, watcher)
   143  	}
   144  
   145  	w.watchers = append(w.watchers, watcher)
   146  	w.listeners = append(w.listeners, listener)
   147  }
   148  
   149  // NotifyWhenUpdated notifies the watcher when a file event is received.
   150  func (w *Watcher) NotifyWhenUpdated(listener Listener, watcher *fsnotify.Watcher) {
   151  	for {
   152  		select {
   153  		case ev := <-watcher.Events:
   154  			if w.rebuildRequired(ev, listener) {
   155  				if w.serial {
   156  					// Serialize listener.Refresh() calls.
   157  					w.notifyMutex.Lock()
   158  
   159  					if err := listener.Refresh(); err != nil {
   160  						utils.Logger.Error("Watcher: Listener refresh reported error:", "error", err)
   161  					}
   162  					w.notifyMutex.Unlock()
   163  				} else {
   164  					// Run refresh in parallel
   165  					go func() {
   166  						if err := w.notifyInProcess(listener); err != nil {
   167  							utils.Logger.Error("failed to notify",
   168  								"error", err)
   169  						}
   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.SourceError {
   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  			"force", w.forceRefresh, "refresh", refresh, "lastError", w.lastError == i)
   210  		if w.forceRefresh || refresh || w.lastError == i {
   211  			var err *utils.SourceError
   212  			if w.serial {
   213  				err = listener.Refresh()
   214  			} else {
   215  				err = w.notifyInProcess(listener)
   216  			}
   217  			if err != nil {
   218  				w.lastError = i
   219  				w.forceRefresh = true
   220  				return err
   221  			}
   222  
   223  			w.lastError = -1
   224  			w.forceRefresh = false
   225  		}
   226  	}
   227  
   228  	return nil
   229  }
   230  
   231  // Build a queue for refresh notifications
   232  // this will not return until one of the queue completes.
   233  func (w *Watcher) notifyInProcess(listener Listener) (err *utils.SourceError) {
   234  	shouldReturn := false
   235  	// This code block ensures that either a timer is created
   236  	// or that a process would be added the the h.refreshChannel
   237  	func() {
   238  		w.timerMutex.Lock()
   239  		defer w.timerMutex.Unlock()
   240  		// If we are in the process of a rebuild, forceRefresh will always be true
   241  		w.forceRefresh = true
   242  		if w.refreshTimer != nil {
   243  			utils.Logger.Info("Found existing timer running, resetting")
   244  			w.refreshTimer.Reset(w.refreshInterval)
   245  			shouldReturn = true
   246  			w.refreshChannelCount++
   247  		} else {
   248  			w.refreshTimer = time.NewTimer(w.refreshInterval)
   249  		}
   250  	}()
   251  
   252  	// If another process is already waiting for the timer this one
   253  	// only needs to return the output from the channel
   254  	if shouldReturn {
   255  		return <-w.refreshChannel
   256  	}
   257  	utils.Logger.Info("Waiting for refresh timer to expire")
   258  	<-w.refreshTimer.C
   259  	w.timerMutex.Lock()
   260  
   261  	// Ensure the queue is properly dispatched even if a panic occurs
   262  	defer func() {
   263  		for x := 0; x < w.refreshChannelCount; x++ {
   264  			w.refreshChannel <- err
   265  		}
   266  		w.refreshChannelCount = 0
   267  		w.refreshTimer = nil
   268  		w.timerMutex.Unlock()
   269  	}()
   270  
   271  	err = listener.Refresh()
   272  	if err != nil {
   273  		utils.Logger.Info("Watcher: Recording error last build, setting rebuild on", "error", err)
   274  	} else {
   275  		w.lastError = -1
   276  		w.forceRefresh = false
   277  	}
   278  	utils.Logger.Info("Rebuilt, result", "error", err)
   279  	return
   280  }
   281  
   282  func (w *Watcher) rebuildRequired(ev fsnotify.Event, listener Listener) bool {
   283  	// Ignore changes to dotfiles.
   284  	if strings.HasPrefix(filepath.Base(ev.Name), ".") {
   285  		return false
   286  	}
   287  
   288  	if dl, ok := listener.(DiscerningListener); ok {
   289  		if !dl.WatchFile(ev.Name) || ev.Op&fsnotify.Chmod == fsnotify.Chmod {
   290  			return false
   291  		}
   292  	}
   293  	return true
   294  }