watcher_naive.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package watch
  14. import (
  15. "fmt"
  16. "io/fs"
  17. "os"
  18. "path/filepath"
  19. "runtime"
  20. "strings"
  21. "github.com/pkg/errors"
  22. "github.com/sirupsen/logrus"
  23. "github.com/tilt-dev/fsnotify"
  24. )
  25. // A naive file watcher that uses the plain fsnotify API.
  26. // Used on all non-Darwin systems (including Windows & Linux).
  27. //
  28. // All OS-specific codepaths are handled by fsnotify.
  29. type naiveNotify struct {
  30. // Paths that we're watching that should be passed up to the caller.
  31. // Note that we may have to watch ancestors of these paths
  32. // in order to fulfill the API promise.
  33. //
  34. // We often need to check if paths are a child of a path in
  35. // the notify list. It might be better to store this in a tree
  36. // structure, so we can filter the list quickly.
  37. notifyList map[string]bool
  38. ignore PathMatcher
  39. isWatcherRecursive bool
  40. watcher *fsnotify.Watcher
  41. events chan fsnotify.Event
  42. wrappedEvents chan FileEvent
  43. errors chan error
  44. numWatches int64
  45. }
  46. func (d *naiveNotify) Start() error {
  47. if len(d.notifyList) == 0 {
  48. return nil
  49. }
  50. pathsToWatch := []string{}
  51. for path := range d.notifyList {
  52. pathsToWatch = append(pathsToWatch, path)
  53. }
  54. pathsToWatch, err := greatestExistingAncestors(pathsToWatch)
  55. if err != nil {
  56. return err
  57. }
  58. if d.isWatcherRecursive {
  59. pathsToWatch = dedupePathsForRecursiveWatcher(pathsToWatch)
  60. }
  61. for _, name := range pathsToWatch {
  62. fi, err := os.Stat(name)
  63. if err != nil && !os.IsNotExist(err) {
  64. return errors.Wrapf(err, "notify.Add(%q)", name)
  65. }
  66. // if it's a file that doesn't exist,
  67. // we should have caught that above, let's just skip it.
  68. if os.IsNotExist(err) {
  69. continue
  70. }
  71. if fi.IsDir() {
  72. err = d.watchRecursively(name)
  73. if err != nil {
  74. return errors.Wrapf(err, "notify.Add(%q)", name)
  75. }
  76. } else {
  77. err = d.add(filepath.Dir(name))
  78. if err != nil {
  79. return errors.Wrapf(err, "notify.Add(%q)", filepath.Dir(name))
  80. }
  81. }
  82. }
  83. go d.loop()
  84. return nil
  85. }
  86. func (d *naiveNotify) watchRecursively(dir string) error {
  87. if d.isWatcherRecursive {
  88. err := d.add(dir)
  89. if err == nil || os.IsNotExist(err) {
  90. return nil
  91. }
  92. return errors.Wrapf(err, "watcher.Add(%q)", dir)
  93. }
  94. return filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
  95. if err != nil {
  96. return err
  97. }
  98. if !info.IsDir() {
  99. return nil
  100. }
  101. shouldSkipDir, err := d.shouldSkipDir(path)
  102. if err != nil {
  103. return err
  104. }
  105. if shouldSkipDir {
  106. return filepath.SkipDir
  107. }
  108. err = d.add(path)
  109. if err != nil {
  110. if os.IsNotExist(err) {
  111. return nil
  112. }
  113. return errors.Wrapf(err, "watcher.Add(%q)", path)
  114. }
  115. return nil
  116. })
  117. }
  118. func (d *naiveNotify) Close() error {
  119. numberOfWatches.Add(-d.numWatches)
  120. d.numWatches = 0
  121. return d.watcher.Close()
  122. }
  123. func (d *naiveNotify) Events() chan FileEvent {
  124. return d.wrappedEvents
  125. }
  126. func (d *naiveNotify) Errors() chan error {
  127. return d.errors
  128. }
  129. func (d *naiveNotify) loop() { //nolint:gocyclo
  130. defer close(d.wrappedEvents)
  131. for e := range d.events {
  132. // The Windows fsnotify event stream sometimes gets events with empty names
  133. // that are also sent to the error stream. Hmmmm...
  134. if e.Name == "" {
  135. continue
  136. }
  137. if e.Op&fsnotify.Create != fsnotify.Create {
  138. if d.shouldNotify(e.Name) {
  139. d.wrappedEvents <- FileEvent{e.Name}
  140. }
  141. continue
  142. }
  143. if d.isWatcherRecursive {
  144. if d.shouldNotify(e.Name) {
  145. d.wrappedEvents <- FileEvent{e.Name}
  146. }
  147. continue
  148. }
  149. // If the watcher is not recursive, we have to walk the tree
  150. // and add watches manually. We fire the event while we're walking the tree.
  151. // because it's a bit more elegant that way.
  152. //
  153. // TODO(dbentley): if there's a delete should we call d.watcher.Remove to prevent leaking?
  154. err := filepath.WalkDir(e.Name, func(path string, info fs.DirEntry, err error) error {
  155. if err != nil {
  156. return err
  157. }
  158. if d.shouldNotify(path) {
  159. d.wrappedEvents <- FileEvent{path}
  160. }
  161. // TODO(dmiller): symlinks 😭
  162. shouldWatch := false
  163. if info.IsDir() {
  164. // watch directories unless we can skip them entirely
  165. shouldSkipDir, err := d.shouldSkipDir(path)
  166. if err != nil {
  167. return err
  168. }
  169. if shouldSkipDir {
  170. return filepath.SkipDir
  171. }
  172. shouldWatch = true
  173. } else {
  174. // watch files that are explicitly named, but don't watch others
  175. _, ok := d.notifyList[path]
  176. if ok {
  177. shouldWatch = true
  178. }
  179. }
  180. if shouldWatch {
  181. err := d.add(path)
  182. if err != nil && !os.IsNotExist(err) {
  183. logrus.Infof("Error watching path %s: %s", e.Name, err)
  184. }
  185. }
  186. return nil
  187. })
  188. if err != nil && !os.IsNotExist(err) {
  189. logrus.Infof("Error walking directory %s: %s", e.Name, err)
  190. }
  191. }
  192. }
  193. func (d *naiveNotify) shouldNotify(path string) bool {
  194. ignore, err := d.ignore.Matches(path)
  195. if err != nil {
  196. logrus.Infof("Error matching path %q: %v", path, err)
  197. } else if ignore {
  198. return false
  199. }
  200. if _, ok := d.notifyList[path]; ok {
  201. // We generally don't care when directories change at the root of an ADD
  202. stat, err := os.Lstat(path)
  203. isDir := err == nil && stat.IsDir()
  204. return !isDir
  205. }
  206. for root := range d.notifyList {
  207. if IsChild(root, path) {
  208. return true
  209. }
  210. }
  211. return false
  212. }
  213. func (d *naiveNotify) shouldSkipDir(path string) (bool, error) {
  214. // If path is directly in the notifyList, we should always watch it.
  215. if d.notifyList[path] {
  216. return false, nil
  217. }
  218. skip, err := d.ignore.MatchesEntireDir(path)
  219. if err != nil {
  220. return false, errors.Wrap(err, "shouldSkipDir")
  221. }
  222. if skip {
  223. return true, nil
  224. }
  225. // Suppose we're watching
  226. // /src/.tiltignore
  227. // but the .tiltignore file doesn't exist.
  228. //
  229. // Our watcher will create an inotify watch on /src/.
  230. //
  231. // But then we want to make sure we don't recurse from /src/ down to /src/node_modules.
  232. //
  233. // To handle this case, we only want to traverse dirs that are:
  234. // - A child of a directory that's in our notify list, or
  235. // - A parent of a directory that's in our notify list
  236. // (i.e., to cover the "path doesn't exist" case).
  237. for root := range d.notifyList {
  238. if IsChild(root, path) || IsChild(path, root) {
  239. return false, nil
  240. }
  241. }
  242. return true, nil
  243. }
  244. func (d *naiveNotify) add(path string) error {
  245. err := d.watcher.Add(path)
  246. if err != nil {
  247. return err
  248. }
  249. d.numWatches++
  250. numberOfWatches.Add(1)
  251. return nil
  252. }
  253. func newWatcher(paths []string, ignore PathMatcher) (*naiveNotify, error) {
  254. if ignore == nil {
  255. return nil, fmt.Errorf("newWatcher: ignore is nil")
  256. }
  257. fsw, err := fsnotify.NewWatcher()
  258. if err != nil {
  259. if strings.Contains(err.Error(), "too many open files") && runtime.GOOS == "linux" {
  260. return nil, fmt.Errorf("Hit OS limits creating a watcher.\n" +
  261. "Run 'sysctl fs.inotify.max_user_instances' to check your inotify limits.\n" +
  262. "To raise them, run 'sudo sysctl fs.inotify.max_user_instances=1024'")
  263. }
  264. return nil, errors.Wrap(err, "creating file watcher")
  265. }
  266. MaybeIncreaseBufferSize(fsw)
  267. err = fsw.SetRecursive()
  268. isWatcherRecursive := err == nil
  269. wrappedEvents := make(chan FileEvent)
  270. notifyList := make(map[string]bool, len(paths))
  271. if isWatcherRecursive {
  272. paths = dedupePathsForRecursiveWatcher(paths)
  273. }
  274. for _, path := range paths {
  275. path, err := filepath.Abs(path)
  276. if err != nil {
  277. return nil, errors.Wrap(err, "newWatcher")
  278. }
  279. notifyList[path] = true
  280. }
  281. wmw := &naiveNotify{
  282. notifyList: notifyList,
  283. ignore: ignore,
  284. watcher: fsw,
  285. events: fsw.Events,
  286. wrappedEvents: wrappedEvents,
  287. errors: fsw.Errors,
  288. isWatcherRecursive: isWatcherRecursive,
  289. }
  290. return wmw, nil
  291. }
  292. var _ Notify = &naiveNotify{}
  293. func greatestExistingAncestors(paths []string) ([]string, error) {
  294. result := []string{}
  295. for _, p := range paths {
  296. newP, err := greatestExistingAncestor(p)
  297. if err != nil {
  298. return nil, fmt.Errorf("Finding ancestor of %s: %v", p, err)
  299. }
  300. result = append(result, newP)
  301. }
  302. return result, nil
  303. }