github.com/icyphox/x@v0.0.355-0.20220311094250-029bd783e8b8/watcherx/file.go (about) 1 package watcherx 2 3 import ( 4 "context" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 9 "github.com/fsnotify/fsnotify" 10 "github.com/pkg/errors" 11 ) 12 13 func WatchFile(ctx context.Context, file string, c EventChannel) (Watcher, error) { 14 watcher, err := fsnotify.NewWatcher() 15 if err != nil { 16 close(c) 17 return nil, errors.WithStack(err) 18 } 19 dir := filepath.Dir(file) 20 if err := watcher.Add(dir); err != nil { 21 close(c) 22 return nil, errors.WithStack(err) 23 } 24 resolvedFile, err := filepath.EvalSymlinks(file) 25 if err != nil { 26 if _, ok := err.(*os.PathError); !ok { 27 close(c) 28 return nil, errors.WithStack(err) 29 } 30 // The file does not exist. The watcher should still watch the directory 31 // to get notified about file creation. 32 resolvedFile = "" 33 } else if resolvedFile != file { 34 // If `resolvedFile` != `file` then `file` is a symlink and we have to explicitly watch the referenced file. 35 // This is because fsnotify follows symlinks and watches the destination file, not the symlink 36 // itself. That is at least the case for unix systems. See: https://github.com/fsnotify/fsnotify/issues/199 37 if err := watcher.Add(file); err != nil { 38 close(c) 39 return nil, errors.WithStack(err) 40 } 41 } 42 d := newDispatcher() 43 go streamFileEvents(ctx, watcher, c, d.trigger, d.done, file, resolvedFile) 44 return d, nil 45 } 46 47 // streamFileEvents watches for file changes and supports symlinks which requires several workarounds due to limitations of fsnotify. 48 // Argument `resolvedFile` is the resolved symlink path of the file, or it is the watchedFile name itself. If `resolvedFile` is empty, then the watchedFile does not exist. 49 func streamFileEvents(ctx context.Context, watcher *fsnotify.Watcher, c EventChannel, sendNow <-chan struct{}, sendNowDone chan<- int, watchedFile, resolvedFile string) { 50 defer close(c) 51 eventSource := source(watchedFile) 52 removeDirectFileWatcher := func() { 53 _ = watcher.Remove(watchedFile) 54 } 55 addDirectFileWatcher := func() { 56 // check if the watchedFile (symlink) exists 57 // if it does not the dir watcher will notify us when it gets created 58 if _, err := os.Lstat(watchedFile); err == nil { 59 if err := watcher.Add(watchedFile); err != nil { 60 c <- &ErrorEvent{ 61 error: errors.WithStack(err), 62 source: eventSource, 63 } 64 } 65 } 66 } 67 for { 68 select { 69 case <-ctx.Done(): 70 _ = watcher.Close() 71 return 72 case <-sendNow: 73 if resolvedFile == "" { 74 // The file does not exist. Announce this by sending a RemoveEvent. 75 c <- &RemoveEvent{eventSource} 76 } else { 77 // The file does exist. Announce the current content by sending a ChangeEvent. 78 data, err := ioutil.ReadFile(watchedFile) 79 if err != nil { 80 c <- &ErrorEvent{ 81 error: errors.WithStack(err), 82 source: eventSource, 83 } 84 continue 85 } 86 c <- &ChangeEvent{ 87 data: data, 88 source: eventSource, 89 } 90 } 91 92 // in any of the above cases we send exactly one event 93 sendNowDone <- 1 94 case e, ok := <-watcher.Events: 95 if !ok { 96 return 97 } 98 // filter events to only watch watchedFile 99 // e.Name contains the name of the watchedFile (regardless whether it is a symlink), not the resolved file name 100 if filepath.Clean(e.Name) == watchedFile { 101 recentlyResolvedFile, err := filepath.EvalSymlinks(watchedFile) 102 // when there is no error the file exists and any symlinks can be resolved 103 if err != nil { 104 // check if the watchedFile (or the file behind the symlink) was removed 105 if _, ok := err.(*os.PathError); ok { 106 c <- &RemoveEvent{eventSource} 107 removeDirectFileWatcher() 108 continue 109 } 110 c <- &ErrorEvent{ 111 error: errors.WithStack(err), 112 source: eventSource, 113 } 114 continue 115 } 116 // This catches following three cases: 117 // 1. the watchedFile was written or created 118 // 2. the watchedFile is a symlink and has changed (k8s config map updates) 119 // 3. the watchedFile behind the symlink was written or created 120 switch { 121 case recentlyResolvedFile != resolvedFile: 122 resolvedFile = recentlyResolvedFile 123 // watch the symlink again to update the actually watched file 124 removeDirectFileWatcher() 125 addDirectFileWatcher() 126 // we fallthrough because we also want to read the file in this case 127 fallthrough 128 case e.Op&(fsnotify.Write|fsnotify.Create) != 0: 129 data, err := ioutil.ReadFile(watchedFile) 130 if err != nil { 131 c <- &ErrorEvent{ 132 error: errors.WithStack(err), 133 source: eventSource, 134 } 135 continue 136 } 137 c <- &ChangeEvent{ 138 data: data, 139 source: eventSource, 140 } 141 } 142 } 143 } 144 } 145 }