watcher_naive.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. // +build !darwin
  2. package watch
  3. import (
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "runtime"
  8. "github.com/pkg/errors"
  9. "github.com/windmilleng/fsnotify"
  10. "github.com/windmilleng/tilt/internal/ospath"
  11. "github.com/windmilleng/tilt/pkg/logger"
  12. )
  13. // A naive file watcher that uses the plain fsnotify API.
  14. // Used on all non-Darwin systems (including Windows & Linux).
  15. //
  16. // All OS-specific codepaths are handled by fsnotify.
  17. type naiveNotify struct {
  18. // Paths that we're watching that should be passed up to the caller.
  19. // Note that we may have to watch ancestors of these paths
  20. // in order to fulfill the API promise.
  21. notifyList map[string]bool
  22. ignore PathMatcher
  23. log logger.Logger
  24. watcher *fsnotify.Watcher
  25. events chan fsnotify.Event
  26. wrappedEvents chan FileEvent
  27. errors chan error
  28. numWatches int64
  29. }
  30. func (d *naiveNotify) Start() error {
  31. if len(d.notifyList) == 0 {
  32. return nil
  33. }
  34. for name := range d.notifyList {
  35. fi, err := os.Stat(name)
  36. if err != nil && !os.IsNotExist(err) {
  37. return errors.Wrapf(err, "notify.Add(%q)", name)
  38. }
  39. // if it's a file that doesn't exist, watch its parent
  40. if os.IsNotExist(err) {
  41. err = d.watchAncestorOfMissingPath(name)
  42. if err != nil {
  43. return errors.Wrapf(err, "watchAncestorOfMissingPath(%q)", name)
  44. }
  45. } else if fi.IsDir() {
  46. err = d.watchRecursively(name)
  47. if err != nil {
  48. return errors.Wrapf(err, "notify.Add(%q)", name)
  49. }
  50. } else {
  51. err = d.add(filepath.Dir(name))
  52. if err != nil {
  53. return errors.Wrapf(err, "notify.Add(%q)", filepath.Dir(name))
  54. }
  55. }
  56. }
  57. go d.loop()
  58. return nil
  59. }
  60. func (d *naiveNotify) watchRecursively(dir string) error {
  61. return filepath.Walk(dir, func(path string, mode os.FileInfo, err error) error {
  62. if err != nil {
  63. return err
  64. }
  65. if !mode.IsDir() {
  66. return nil
  67. }
  68. shouldSkipDir, err := d.shouldSkipDir(path)
  69. if err != nil {
  70. return err
  71. }
  72. if shouldSkipDir {
  73. return filepath.SkipDir
  74. }
  75. err = d.add(path)
  76. if err != nil {
  77. if os.IsNotExist(err) {
  78. return nil
  79. }
  80. return errors.Wrapf(err, "watcher.Add(%q)", path)
  81. }
  82. return nil
  83. })
  84. }
  85. func (d *naiveNotify) watchAncestorOfMissingPath(path string) error {
  86. if path == string(filepath.Separator) {
  87. return fmt.Errorf("cannot watch root directory")
  88. }
  89. _, err := os.Stat(path)
  90. if err != nil && !os.IsNotExist(err) {
  91. return errors.Wrapf(err, "os.Stat(%q)", path)
  92. }
  93. if os.IsNotExist(err) {
  94. parent := filepath.Dir(path)
  95. return d.watchAncestorOfMissingPath(parent)
  96. }
  97. return d.add(path)
  98. }
  99. func (d *naiveNotify) Close() error {
  100. numberOfWatches.Add(-d.numWatches)
  101. d.numWatches = 0
  102. return d.watcher.Close()
  103. }
  104. func (d *naiveNotify) Events() chan FileEvent {
  105. return d.wrappedEvents
  106. }
  107. func (d *naiveNotify) Errors() chan error {
  108. return d.errors
  109. }
  110. func (d *naiveNotify) loop() {
  111. defer close(d.wrappedEvents)
  112. for e := range d.events {
  113. if e.Op&fsnotify.Create != fsnotify.Create {
  114. if d.shouldNotify(e.Name) && !isSpuriousWindowsDirChange(e) {
  115. d.wrappedEvents <- FileEvent{e.Name}
  116. }
  117. continue
  118. }
  119. // TODO(dbentley): if there's a delete should we call d.watcher.Remove to prevent leaking?
  120. err := filepath.Walk(e.Name, func(path string, mode os.FileInfo, err error) error {
  121. if err != nil {
  122. return err
  123. }
  124. if d.shouldNotify(path) {
  125. d.wrappedEvents <- FileEvent{path}
  126. }
  127. // TODO(dmiller): symlinks 😭
  128. shouldWatch := false
  129. if mode.IsDir() {
  130. // watch directories unless we can skip them entirely
  131. shouldSkipDir, err := d.shouldSkipDir(path)
  132. if err != nil {
  133. return err
  134. }
  135. if shouldSkipDir {
  136. return filepath.SkipDir
  137. }
  138. shouldWatch = true
  139. } else {
  140. // watch files that are explicitly named, but don't watch others
  141. _, ok := d.notifyList[path]
  142. if ok {
  143. shouldWatch = true
  144. }
  145. }
  146. if shouldWatch {
  147. err := d.add(path)
  148. if err != nil && !os.IsNotExist(err) {
  149. d.log.Infof("Error watching path %s: %s", e.Name, err)
  150. }
  151. }
  152. return nil
  153. })
  154. if err != nil && !os.IsNotExist(err) {
  155. d.log.Infof("Error walking directory %s: %s", e.Name, err)
  156. }
  157. }
  158. }
  159. func (d *naiveNotify) shouldNotify(path string) bool {
  160. ignore, err := d.ignore.Matches(path)
  161. if err != nil {
  162. d.log.Infof("Error matching path %q: %v", path, err)
  163. } else if ignore {
  164. return false
  165. }
  166. if _, ok := d.notifyList[path]; ok {
  167. return true
  168. }
  169. // TODO(dmiller): maybe use a prefix tree here?
  170. for root := range d.notifyList {
  171. if ospath.IsChild(root, path) {
  172. return true
  173. }
  174. }
  175. return false
  176. }
  177. func (d *naiveNotify) shouldSkipDir(path string) (bool, error) {
  178. // If path is directly in the notifyList, we should always watch it.
  179. if d.notifyList[path] {
  180. return false, nil
  181. }
  182. skip, err := d.ignore.MatchesEntireDir(path)
  183. if err != nil {
  184. return false, errors.Wrap(err, "shouldSkipDir")
  185. }
  186. return skip, nil
  187. }
  188. func (d *naiveNotify) add(path string) error {
  189. err := d.watcher.Add(path)
  190. if err != nil {
  191. return err
  192. }
  193. d.numWatches++
  194. numberOfWatches.Add(1)
  195. return nil
  196. }
  197. func newWatcher(paths []string, ignore PathMatcher, l logger.Logger) (*naiveNotify, error) {
  198. if ignore == nil {
  199. return nil, fmt.Errorf("newWatcher: ignore is nil")
  200. }
  201. fsw, err := fsnotify.NewWatcher()
  202. if err != nil {
  203. return nil, err
  204. }
  205. wrappedEvents := make(chan FileEvent)
  206. notifyList := make(map[string]bool, len(paths))
  207. for _, path := range paths {
  208. path, err := filepath.Abs(path)
  209. if err != nil {
  210. return nil, errors.Wrap(err, "newWatcher")
  211. }
  212. notifyList[path] = true
  213. }
  214. wmw := &naiveNotify{
  215. notifyList: notifyList,
  216. ignore: ignore,
  217. log: l,
  218. watcher: fsw,
  219. events: fsw.Events,
  220. wrappedEvents: wrappedEvents,
  221. errors: fsw.Errors,
  222. }
  223. return wmw, nil
  224. }
  225. // Windows' inotify implementation sometimes fires
  226. // of spurious WRITE events on directories when the
  227. // files inside change.
  228. func isSpuriousWindowsDirChange(e fsnotify.Event) bool {
  229. if runtime.GOOS != "windows" {
  230. return false
  231. }
  232. if e.Op != fsnotify.Write {
  233. return false
  234. }
  235. eIsDir, _ := isDir(e.Name)
  236. return eIsDir
  237. }
  238. func isDir(pth string) (bool, error) {
  239. fi, err := os.Lstat(pth)
  240. if os.IsNotExist(err) {
  241. return false, nil
  242. } else if err != nil {
  243. return false, err
  244. }
  245. return fi.IsDir(), nil
  246. }
  247. var _ Notify = &naiveNotify{}