watcher_darwin.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. package watch
  2. import (
  3. "path/filepath"
  4. "sync"
  5. "time"
  6. "github.com/windmilleng/tilt/internal/logger"
  7. "github.com/windmilleng/tilt/internal/ospath"
  8. "github.com/windmilleng/fsevents"
  9. )
  10. // A file watcher optimized for Darwin.
  11. // Uses FSEvents to avoid the terrible perf characteristics of kqueue.
  12. type darwinNotify struct {
  13. stream *fsevents.EventStream
  14. events chan FileEvent
  15. errors chan error
  16. stop chan struct{}
  17. // TODO(nick): This mutex is needed for the case where we add paths after we
  18. // start watching. But because fsevents supports recursive watches, we don't
  19. // actually need this feature. We should change the api contract of wmNotify
  20. // so that, for recursive watches, we can guarantee that the path list doesn't
  21. // change.
  22. sm *sync.Mutex
  23. pathsWereWatching map[string]interface{}
  24. sawAnyHistoryDone bool
  25. }
  26. func (d *darwinNotify) loop() {
  27. for {
  28. select {
  29. case <-d.stop:
  30. return
  31. case events, ok := <-d.stream.Events:
  32. if !ok {
  33. return
  34. }
  35. for _, e := range events {
  36. e.Path = filepath.Join("/", e.Path)
  37. if e.Flags&fsevents.HistoryDone == fsevents.HistoryDone {
  38. d.sm.Lock()
  39. d.sawAnyHistoryDone = true
  40. d.sm.Unlock()
  41. continue
  42. }
  43. // We wait until we've seen the HistoryDone event for this watcher before processing any events
  44. // so that we skip all of the "spurious" events that precede it.
  45. if !d.sawAnyHistoryDone {
  46. continue
  47. }
  48. _, isPathWereWatching := d.pathsWereWatching[e.Path]
  49. if e.Flags&fsevents.ItemIsDir == fsevents.ItemIsDir && e.Flags&fsevents.ItemCreated == fsevents.ItemCreated && isPathWereWatching {
  50. // This is the first create for the path that we're watching. We always get exactly one of these
  51. // even after we get the HistoryDone event. Skip it.
  52. continue
  53. }
  54. d.events <- FileEvent{
  55. Path: e.Path,
  56. }
  57. }
  58. }
  59. }
  60. }
  61. func (d *darwinNotify) Add(name string) error {
  62. d.sm.Lock()
  63. defer d.sm.Unlock()
  64. es := d.stream
  65. // Check if this is a subdirectory of any of the paths
  66. // we're already watching.
  67. for _, parent := range es.Paths {
  68. if ospath.IsChild(parent, name) {
  69. return nil
  70. }
  71. }
  72. es.Paths = append(es.Paths, name)
  73. if d.pathsWereWatching == nil {
  74. d.pathsWereWatching = make(map[string]interface{})
  75. }
  76. d.pathsWereWatching[name] = struct{}{}
  77. if len(es.Paths) == 1 {
  78. es.Start()
  79. go d.loop()
  80. } else {
  81. es.Restart()
  82. }
  83. return nil
  84. }
  85. func (d *darwinNotify) Close() error {
  86. d.sm.Lock()
  87. defer d.sm.Unlock()
  88. d.stream.Stop()
  89. close(d.errors)
  90. close(d.stop)
  91. return nil
  92. }
  93. func (d *darwinNotify) Events() chan FileEvent {
  94. return d.events
  95. }
  96. func (d *darwinNotify) Errors() chan error {
  97. return d.errors
  98. }
  99. func NewWatcher(l logger.Logger) (Notify, error) {
  100. dw := &darwinNotify{
  101. stream: &fsevents.EventStream{
  102. Latency: 1 * time.Millisecond,
  103. Flags: fsevents.FileEvents,
  104. // NOTE(dmiller): this corresponds to the `sinceWhen` parameter in FSEventStreamCreate
  105. // https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate
  106. EventID: fsevents.LatestEventID(),
  107. },
  108. sm: &sync.Mutex{},
  109. events: make(chan FileEvent),
  110. errors: make(chan error),
  111. stop: make(chan struct{}),
  112. }
  113. return dw, nil
  114. }
  115. var _ Notify = &darwinNotify{}