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 }