watcher_naive.go 7.9 KB

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