github.com/nycdavid/zeus@v0.0.0-20201208104106-9ba439429e03/go/filemonitor/filemonitor_darwin.go (about) 1 // +build darwin 2 3 package filemonitor 4 5 import ( 6 "os" 7 "path/filepath" 8 "time" 9 10 "github.com/fsnotify/fsevents" 11 ) 12 13 const flagsWorthReloadingFor = fsevents.ItemRemoved | fsevents.ItemModified | fsevents.ItemRenamed 14 15 type fsEventsMonitor struct { 16 fileMonitor 17 stream *fsevents.EventStream 18 add chan string 19 stop chan struct{} 20 } 21 22 func NewFileMonitor(fileChangeDelay time.Duration) (FileMonitor, error) { 23 f := fsEventsMonitor{ 24 stream: &fsevents.EventStream{ 25 Paths: []string{}, 26 Latency: fileChangeDelay, 27 Flags: fsevents.FileEvents, 28 EventID: fsevents.EventIDSinceNow, 29 }, 30 // Restarting FSEvents can take ~100ms so buffer adds 31 // in the channel so they can be grouped together. 32 add: make(chan string, 5000), 33 stop: make(chan struct{}), 34 } 35 36 go f.handleAdd() 37 38 return &f, nil 39 } 40 41 func (f *fsEventsMonitor) Add(file string) error { 42 f.add <- file 43 return nil 44 } 45 46 func (f *fsEventsMonitor) Close() error { 47 select { 48 case <-f.stop: 49 return nil // Already stopped 50 default: 51 close(f.stop) 52 close(f.add) 53 } 54 55 return nil 56 } 57 58 func (f *fsEventsMonitor) watch() { 59 for { 60 select { 61 case events := <-f.stream.Events: 62 paths := make([]string, 0, len(events)) 63 for _, event := range events { 64 if (event.Flags & (fsevents.ItemIsFile | flagsWorthReloadingFor)) == 0 { 65 continue 66 } 67 68 paths = append(paths, event.Path) 69 } 70 71 if len(paths) == 0 { 72 continue 73 } 74 75 for _, l := range f.listeners { 76 l <- paths 77 } 78 case <-f.stop: 79 return 80 } 81 } 82 } 83 84 func (f *fsEventsMonitor) handleAdd() { 85 watched := make(map[string]bool) 86 started := false 87 // We don't want to add individual files to watch here but figure out the 88 // directory to watch and watch it instead. 89 90 for file := range f.add { 91 path, err := pathToMonitor(file) 92 if err != nil { 93 // can't access the file for some reason, best to ignore, probably should 94 // log something here 95 continue 96 } 97 98 if watched[path] { 99 continue 100 } 101 102 allFiles := []string{path} 103 104 // Read all messages waiting in the channel so we can batch restarts 105 done := false 106 for !done { 107 select { 108 case file := <-f.add: 109 path, err := pathToMonitor(file) 110 if err != nil { 111 continue 112 } 113 114 if watched[path] { 115 continue 116 } 117 118 allFiles = append(allFiles, path) 119 default: 120 done = true 121 } 122 } 123 124 for _, file := range allFiles { 125 watched[file] = true 126 if contains(f.stream.Paths, file) { 127 continue 128 } 129 130 f.stream.Paths = append(f.stream.Paths, file) 131 } 132 133 if started { 134 f.stream.Restart() 135 } else { 136 f.stream.Start() 137 go f.watch() 138 started = true 139 } 140 } 141 142 if started { 143 f.stream.Stop() 144 } 145 } 146 147 func pathToMonitor(rawPath string) (path string, err error) { 148 stat, err := os.Stat(rawPath) 149 if err != nil { 150 return 151 } 152 153 if stat.IsDir() { 154 path = rawPath 155 } else { 156 path = filepath.Dir(rawPath) 157 } 158 159 return 160 } 161 162 func contains(s []string, e string) bool { 163 for _, a := range s { 164 if a == e { 165 return true 166 } 167 } 168 169 return false 170 }