github.com/gofunct/common@v0.0.0-20190131174352-fd058c7fbf22/pkg/fs/watcher/fswatch/watcher.go (about) 1 package fswatch 2 3 import ( 4 "os" 5 "path/filepath" 6 "strings" 7 "time" 8 9 "github.com/mgutz/str" 10 ) 11 12 // Watcher represents a file system watcher. It should be initialised 13 // with NewWatcher or NewAutoWatcher, and started with Watcher.Start(). 14 type Watcher struct { 15 paths map[string]*watchItem 16 cnotify chan *Notification 17 cadd chan *watchItem 18 autoWatch bool 19 20 // ignoreFn is used to ignore paths 21 IgnorePathFn func(path string) bool 22 } 23 24 // newWatcher is the internal function for properly setting up a new watcher. 25 func newWatcher(dirNotify bool, initpaths ...string) (w *Watcher) { 26 w = new(Watcher) 27 w.autoWatch = dirNotify 28 w.paths = make(map[string]*watchItem, 0) 29 w.IgnorePathFn = ignorePathDefault 30 31 var paths []string 32 for _, path := range initpaths { 33 matches, err := filepath.Glob(path) 34 if err != nil { 35 continue 36 } 37 paths = append(paths, matches...) 38 } 39 if dirNotify { 40 w.syncAddPaths(paths...) 41 } else { 42 for _, path := range paths { 43 w.paths[path] = watchPath(path) 44 } 45 } 46 return 47 } 48 49 // NewWatcher initialises a new Watcher with an initial set of paths. It 50 // does not start listening, and this Watcher will not automatically add 51 // files created under any directories it is watching. 52 func NewWatcher(paths ...string) *Watcher { 53 return newWatcher(false, paths...) 54 } 55 56 // NewAutoWatcher initialises a new Watcher with an initial set of paths. 57 // It behaves the same as NewWatcher, except it will automatically add 58 // files created in directories it is watching, including adding any 59 // subdirectories. 60 func NewAutoWatcher(paths ...string) *Watcher { 61 return newWatcher(true, paths...) 62 } 63 64 // Start begins watching the files, sending notifications when files change. 65 // It returns a channel that notifications are sent on. 66 func (w *Watcher) Start() <-chan *Notification { 67 if w.cnotify != nil { 68 return w.cnotify 69 } 70 if w.autoWatch { 71 w.cadd = make(chan *watchItem, NotificationBufLen) 72 go w.watchItemListener() 73 } 74 w.cnotify = make(chan *Notification, NotificationBufLen) 75 go w.watch(w.cnotify) 76 return w.cnotify 77 } 78 79 // Stop listening for changes to the files. 80 func (w *Watcher) Stop() { 81 if w.cnotify != nil { 82 close(w.cnotify) 83 } 84 85 if w.cadd != nil { 86 close(w.cadd) 87 } 88 } 89 90 // Active returns true if the Watcher is actively looking for changes. 91 func (w *Watcher) Active() bool { 92 return w.paths != nil && len(w.paths) > 0 93 } 94 95 // Add method takes a variable number of string arguments and adds those 96 // files to the watch list, returning the number of files added. 97 func (w *Watcher) Add(inpaths ...string) { 98 var paths []string 99 for _, path := range inpaths { 100 matches, err := filepath.Glob(path) 101 if err != nil { 102 continue 103 } 104 paths = append(paths, matches...) 105 } 106 if w.autoWatch && w.cnotify != nil { 107 for _, path := range paths { 108 wi := watchPath(path) 109 w.addPaths(wi) 110 } 111 } else if w.autoWatch { 112 w.syncAddPaths(paths...) 113 } else { 114 for _, path := range paths { 115 w.paths[path] = watchPath(path) 116 } 117 } 118 } 119 120 // goroutine that cycles through the list of paths and checks for updates. 121 func (w *Watcher) watch(sndch chan<- *Notification) { 122 defer func() { 123 recover() 124 }() 125 126 for { 127 //fmt.Printf("updating watch info %s\n", time.Now()) 128 <-time.After(WatchDelay) 129 130 for _, wi := range w.paths { 131 //fmt.Printf("cheecking %#v\n", wi.Path) 132 133 if wi.Update() && w.shouldNotify(wi) { 134 sndch <- wi.Notification() 135 } 136 137 if wi.LastEvent == NOEXIST && w.autoWatch { 138 delete(w.paths, wi.Path) 139 } 140 141 if len(w.paths) == 0 { 142 w.Stop() 143 } 144 // if filepath.Base(wi.Path) == "sub1.txt" { 145 // fmt.Printf("%s\n", wi.Path) 146 // } 147 } 148 } 149 } 150 151 func (w *Watcher) shouldNotify(wi *watchItem) bool { 152 if w.autoWatch && wi.StatInfo.IsDir() && 153 !(wi.LastEvent == DELETED || wi.LastEvent == NOEXIST) { 154 go w.addPaths(wi) 155 return false 156 } 157 return true 158 } 159 160 func (w *Watcher) addPaths(wi *watchItem) { 161 walker := getWalker(w, wi.Path, w.cadd) 162 go filepath.Walk(wi.Path, walker) 163 } 164 165 func (w *Watcher) watchItemListener() { 166 defer func() { 167 recover() 168 }() 169 for { 170 wi := <-w.cadd 171 if wi == nil { 172 continue 173 } else if _, watching := w.paths[wi.Path]; watching { 174 continue 175 } 176 w.paths[wi.Path] = wi 177 } 178 } 179 180 func getWalker(w *Watcher, root string, addch chan<- *watchItem) func(string, os.FileInfo, error) error { 181 walker := func(path string, info os.FileInfo, err error) error { 182 if w.IgnorePathFn(path) { 183 if info.IsDir() { 184 //fmt.Println("SKIPPING dir", path) 185 return filepath.SkipDir 186 } 187 return nil 188 } 189 if err != nil { 190 return err 191 } 192 if path == root { 193 return nil 194 } 195 wi := watchPath(path) 196 if wi == nil { 197 return nil 198 } else if _, watching := w.paths[wi.Path]; !watching { 199 wi.LastEvent = CREATED 200 w.cnotify <- wi.Notification() 201 addch <- wi 202 if !wi.StatInfo.IsDir() { 203 return nil 204 } 205 w.addPaths(wi) 206 } 207 return nil 208 } 209 return walker 210 } 211 212 // DefaultIsIgnorePath checks whether a path is ignored. Currently defaults 213 // to hidden files on *nix systems, ie they start with a ".". 214 func ignorePathDefault(path string) bool { 215 if strings.HasPrefix(path, ".") || strings.Contains(path, "/.") { 216 return true 217 } 218 219 // ignore node 220 if strings.HasPrefix(path, "node_modules") || strings.Contains(path, "/node_modules") { 221 return true 222 } 223 224 // vim creates random numeric files 225 base := filepath.Base(path) 226 if str.IsNumeric(base) { 227 return true 228 } 229 return false 230 } 231 232 func (w *Watcher) syncAddPaths(paths ...string) { 233 for _, path := range paths { 234 if w.IgnorePathFn(path) { 235 //fmt.Println("SKIPPING path", path) 236 continue 237 } 238 wi := watchPath(path) 239 if wi == nil { 240 continue 241 } else if wi.LastEvent == NOEXIST { 242 continue 243 } else if _, watching := w.paths[wi.Path]; watching { 244 continue 245 } 246 w.paths[wi.Path] = wi 247 if wi.StatInfo.IsDir() { 248 w.syncAddDir(wi) 249 } 250 } 251 } 252 253 func (w *Watcher) syncAddDir(wi *watchItem) { 254 walker := func(path string, info os.FileInfo, err error) error { 255 if w.IgnorePathFn(path) { 256 if info.IsDir() { 257 //fmt.Println("SKIPPING dir", path) 258 return filepath.SkipDir 259 } 260 return nil 261 } 262 263 if err != nil { 264 return err 265 } 266 if path == wi.Path { 267 return nil 268 } 269 newWI := watchPath(path) 270 if newWI != nil { 271 w.paths[path] = newWI 272 if !newWI.StatInfo.IsDir() { 273 return nil 274 } 275 if _, watching := w.paths[newWI.Path]; !watching { 276 w.syncAddDir(newWI) 277 } 278 } 279 return nil 280 } 281 filepath.Walk(wi.Path, walker) 282 } 283 284 // Watching returns a list of the files being watched. 285 func (w *Watcher) Watching() (paths []string) { 286 paths = make([]string, 0) 287 for path := range w.paths { 288 paths = append(paths, path) 289 } 290 return 291 } 292 293 // State returns a slice of Notifications representing the files being watched 294 // and their last event. 295 func (w *Watcher) State() (state []Notification) { 296 state = make([]Notification, 0) 297 if w.paths == nil { 298 return 299 } 300 for _, wi := range w.paths { 301 state = append(state, *wi.Notification()) 302 } 303 return 304 }