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  }