github.com/nxadm/tail@v1.4.12-0.20231010141446-ba755e4d73b6/watch/inotify_tracker.go (about)

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