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