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 */