github.com/wowker/tail@v0.0.0-20210121082357-fe86e3c1032a/watch/inotify_tracker.go (about)

     1  // Copyright (c) 2015 HPE Software Inc. All rights reserved.
     2  // Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
     3  
     4  package watch
     5  
     6  import (
     7  	"github.com/fsnotify/fsnotify"
     8  	"github.com/wowker/tail/util"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"sync"
    13  	"syscall"
    14  )
    15  
    16  type InotifyTracker struct {
    17  	mux       sync.Mutex
    18  	watcher   *fsnotify.Watcher
    19  	chans     map[string]chan fsnotify.Event
    20  	done      map[string]chan bool
    21  	watchNums map[string]int
    22  	watch     chan *watchInfo
    23  	remove    chan *watchInfo
    24  	error     chan error
    25  }
    26  
    27  type watchInfo struct {
    28  	op    fsnotify.Op
    29  	fname string
    30  }
    31  
    32  func (this *watchInfo) isCreate() bool {
    33  	return this.op == fsnotify.Create
    34  }
    35  
    36  var (
    37  	// globally shared InotifyTracker; ensures only one fsnotify.Watcher is used
    38  	shared *InotifyTracker
    39  
    40  	// these are used to ensure the shared InotifyTracker is run exactly once
    41  	once  = sync.Once{}
    42  	goRun = func() {
    43  		shared = &InotifyTracker{
    44  			mux:       sync.Mutex{},
    45  			chans:     make(map[string]chan fsnotify.Event),
    46  			done:      make(map[string]chan bool),
    47  			watchNums: make(map[string]int),
    48  			watch:     make(chan *watchInfo),
    49  			remove:    make(chan *watchInfo),
    50  			error:     make(chan error),
    51  		}
    52  		go shared.run()
    53  	}
    54  
    55  	logger = log.New(os.Stderr, "", log.LstdFlags)
    56  )
    57  
    58  // Watch signals the run goroutine to begin watching the input filename
    59  func Watch(fname string) error {
    60  	return watch(&watchInfo{
    61  		fname: fname,
    62  	})
    63  }
    64  
    65  // Watch create signals the run goroutine to begin watching the input filename
    66  // if call the WatchCreate function, don't call the Cleanup, call the RemoveWatchCreate
    67  func WatchCreate(fname string) error {
    68  	return watch(&watchInfo{
    69  		op:    fsnotify.Create,
    70  		fname: fname,
    71  	})
    72  }
    73  
    74  func watch(winfo *watchInfo) error {
    75  	// start running the shared InotifyTracker if not already running
    76  	once.Do(goRun)
    77  
    78  	winfo.fname = filepath.Clean(winfo.fname)
    79  	shared.watch <- winfo
    80  	return <-shared.error
    81  }
    82  
    83  // RemoveWatch signals the run goroutine to remove the watch for the input filename
    84  func RemoveWatch(fname string) error {
    85  	return remove(&watchInfo{
    86  		fname: fname,
    87  	})
    88  }
    89  
    90  // RemoveWatch create signals the run goroutine to remove the watch for the input filename
    91  func RemoveWatchCreate(fname string) error {
    92  	return remove(&watchInfo{
    93  		op:    fsnotify.Create,
    94  		fname: fname,
    95  	})
    96  }
    97  
    98  func remove(winfo *watchInfo) error {
    99  	// start running the shared InotifyTracker if not already running
   100  	once.Do(goRun)
   101  
   102  	winfo.fname = filepath.Clean(winfo.fname)
   103  	shared.mux.Lock()
   104  	done := shared.done[winfo.fname]
   105  	if done != nil {
   106  		delete(shared.done, winfo.fname)
   107  		close(done)
   108  	}
   109  	shared.mux.Unlock()
   110  
   111  	shared.remove <- winfo
   112  	return <-shared.error
   113  }
   114  
   115  // Events returns a channel to which FileEvents corresponding to the input filename
   116  // will be sent. This channel will be closed when removeWatch is called on this
   117  // filename.
   118  func Events(fname string) <-chan fsnotify.Event {
   119  	shared.mux.Lock()
   120  	defer shared.mux.Unlock()
   121  
   122  	return shared.chans[fname]
   123  }
   124  
   125  // Cleanup removes the watch for the input filename if necessary.
   126  func Cleanup(fname string) error {
   127  	return RemoveWatch(fname)
   128  }
   129  
   130  // watchFlags calls fsnotify.WatchFlags for the input filename and flags, creating
   131  // a new Watcher if the previous Watcher was closed.
   132  func (shared *InotifyTracker) addWatch(winfo *watchInfo) error {
   133  	shared.mux.Lock()
   134  	defer shared.mux.Unlock()
   135  
   136  	if shared.chans[winfo.fname] == nil {
   137  		shared.chans[winfo.fname] = make(chan fsnotify.Event)
   138  	}
   139  	if shared.done[winfo.fname] == nil {
   140  		shared.done[winfo.fname] = make(chan bool)
   141  	}
   142  
   143  	fname := winfo.fname
   144  	if winfo.isCreate() {
   145  		// Watch for new files to be created in the parent directory.
   146  		fname = filepath.Dir(fname)
   147  	}
   148  
   149  	var err error
   150  	// already in inotify watch
   151  	if shared.watchNums[fname] == 0 {
   152  		err = shared.watcher.Add(fname)
   153  	}
   154  	if err == nil {
   155  		shared.watchNums[fname]++
   156  	}
   157  	return err
   158  }
   159  
   160  // removeWatch calls fsnotify.RemoveWatch for the input filename and closes the
   161  // corresponding events channel.
   162  func (shared *InotifyTracker) removeWatch(winfo *watchInfo) error {
   163  	shared.mux.Lock()
   164  
   165  	ch := shared.chans[winfo.fname]
   166  	if ch != nil {
   167  		delete(shared.chans, winfo.fname)
   168  		close(ch)
   169  	}
   170  
   171  	fname := winfo.fname
   172  	if winfo.isCreate() {
   173  		// Watch for new files to be created in the parent directory.
   174  		fname = filepath.Dir(fname)
   175  	}
   176  	shared.watchNums[fname]--
   177  	watchNum := shared.watchNums[fname]
   178  	if watchNum == 0 {
   179  		delete(shared.watchNums, fname)
   180  	}
   181  	shared.mux.Unlock()
   182  
   183  	var err error
   184  	// If we were the last ones to watch this file, unsubscribe from inotify.
   185  	// This needs to happen after releasing the lock because fsnotify waits
   186  	// synchronously for the kernel to acknowledge the removal of the watch
   187  	// for this file, which causes us to deadlock if we still held the lock.
   188  	if watchNum == 0 {
   189  		err = shared.watcher.Remove(fname)
   190  	}
   191  
   192  	return err
   193  }
   194  
   195  // sendEvent sends the input event to the appropriate Tail.
   196  func (shared *InotifyTracker) sendEvent(event fsnotify.Event) {
   197  	name := filepath.Clean(event.Name)
   198  
   199  	shared.mux.Lock()
   200  	ch := shared.chans[name]
   201  	done := shared.done[name]
   202  	shared.mux.Unlock()
   203  
   204  	if ch != nil && done != nil {
   205  		select {
   206  		case ch <- event:
   207  		case <-done:
   208  		}
   209  	}
   210  }
   211  
   212  // run starts the goroutine in which the shared struct reads events from its
   213  // Watcher's Event channel and sends the events to the appropriate Tail.
   214  func (shared *InotifyTracker) run() {
   215  	watcher, err := fsnotify.NewWatcher()
   216  	if err != nil {
   217  		util.Fatal("failed to create Watcher")
   218  	}
   219  	shared.watcher = watcher
   220  
   221  	for {
   222  		select {
   223  		case winfo := <-shared.watch:
   224  			shared.error <- shared.addWatch(winfo)
   225  
   226  		case winfo := <-shared.remove:
   227  			shared.error <- shared.removeWatch(winfo)
   228  
   229  		case event, open := <-shared.watcher.Events:
   230  			if !open {
   231  				return
   232  			}
   233  			shared.sendEvent(event)
   234  
   235  		case err, open := <-shared.watcher.Errors:
   236  			if !open {
   237  				return
   238  			} else if err != nil {
   239  				sysErr, ok := err.(*os.SyscallError)
   240  				if !ok || sysErr.Err != syscall.EINTR {
   241  					logger.Printf("Error in Watcher Error channel: %s", err)
   242  				}
   243  			}
   244  		}
   245  	}
   246  }