github.com/grafana/tail@v0.0.0-20230510142333-77b18831edf0/watch/polling.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  	"fmt"
     8  	"os"
     9  	"runtime"
    10  	"time"
    11  
    12  	"github.com/grafana/tail/util"
    13  	"gopkg.in/tomb.v1"
    14  )
    15  
    16  // PollingFileWatcher polls the file for changes.
    17  type PollingFileWatcher struct {
    18  	File     *os.File
    19  	Filename string
    20  	Size     int64
    21  	Options  PollingFileWatcherOptions
    22  }
    23  
    24  // PollingFileWatcherOptions customizes a PollingFileWatcher.
    25  type PollingFileWatcherOptions struct {
    26  	// MinPollFrequency and MaxPollFrequency specify how frequently a
    27  	// PollingFileWatcher should poll the file.
    28  	//
    29  	// PollingFileWatcher starts polling at MinPollFrequency, and will
    30  	// exponentially increase the polling frequency up to MaxPollFrequency if no
    31  	// new entries are found. The polling frequency is reset to MinPollFrequency
    32  	// whenever a new log entry is found or if the polled file changes.
    33  	MinPollFrequency, MaxPollFrequency time.Duration
    34  }
    35  
    36  // DefaultPollingFileWatcherOptions holds default values for
    37  // PollingFileWatcherOptions.
    38  var DefaultPollingFileWatcherOptions = PollingFileWatcherOptions{
    39  	MinPollFrequency: 250 * time.Millisecond,
    40  	MaxPollFrequency: 250 * time.Millisecond,
    41  }
    42  
    43  func NewPollingFileWatcher(filename string, opts PollingFileWatcherOptions) (*PollingFileWatcher, error) {
    44  	if opts == (PollingFileWatcherOptions{}) {
    45  		opts = DefaultPollingFileWatcherOptions
    46  	}
    47  
    48  	if opts.MinPollFrequency == 0 || opts.MaxPollFrequency == 0 {
    49  		return nil, fmt.Errorf("MinPollFrequency and MaxPollFrequency must be greater than 0")
    50  	} else if opts.MaxPollFrequency < opts.MinPollFrequency {
    51  		return nil, fmt.Errorf("MaxPollFrequency must be larger than MinPollFrequency")
    52  	}
    53  
    54  	return &PollingFileWatcher{
    55  		File:     nil,
    56  		Filename: filename,
    57  		Size:     0,
    58  		Options:  opts,
    59  	}, nil
    60  }
    61  
    62  func (fw *PollingFileWatcher) BlockUntilExists(t *tomb.Tomb) error {
    63  	bo := newPollBackoff(fw.Options)
    64  
    65  	for {
    66  		if _, err := os.Stat(fw.Filename); err == nil {
    67  			return nil
    68  		} else if !os.IsNotExist(err) {
    69  			return err
    70  		}
    71  		select {
    72  		case <-time.After(bo.WaitTime()):
    73  			bo.Backoff()
    74  			continue
    75  		case <-t.Dying():
    76  			return tomb.ErrDying
    77  		}
    78  	}
    79  	panic("unreachable")
    80  }
    81  
    82  func (fw *PollingFileWatcher) ChangeEvents(t *tomb.Tomb, pos int64) (*FileChanges, error) {
    83  	origFi, err := os.Stat(fw.Filename)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	changes := NewFileChanges()
    89  	var prevModTime time.Time
    90  
    91  	// XXX: use tomb.Tomb to cleanly manage these goroutines. replace
    92  	// the fatal (below) with tomb's Kill.
    93  
    94  	fw.Size = pos
    95  
    96  	bo := newPollBackoff(fw.Options)
    97  
    98  	go func() {
    99  		prevSize := fw.Size
   100  		for {
   101  			select {
   102  			case <-t.Dying():
   103  				return
   104  			default:
   105  			}
   106  
   107  			time.Sleep(bo.WaitTime())
   108  			deletePending, err := IsDeletePending(fw.File)
   109  
   110  			// DeletePending is a windows state where the file has been queued
   111  			// for delete but won't actually get deleted until all handles are
   112  			// closed. It's a variation on the NotifyDeleted call below.
   113  			//
   114  			// IsDeletePending may fail in cases where the file handle becomes
   115  			// invalid, so we treat a failed call the same as a pending delete.
   116  			if err != nil || deletePending {
   117  				fw.closeFile()
   118  				changes.NotifyDeleted()
   119  				return
   120  			}
   121  
   122  			fi, err := os.Stat(fw.Filename)
   123  			if err != nil {
   124  				// Windows cannot delete a file if a handle is still open (tail keeps one open)
   125  				// so it gives access denied to anything trying to read it until all handles are released.
   126  				if os.IsNotExist(err) || (runtime.GOOS == "windows" && os.IsPermission(err)) {
   127  					// File does not exist (has been deleted).
   128  					changes.NotifyDeleted()
   129  					return
   130  				}
   131  
   132  				// XXX: report this error back to the user
   133  				util.Fatal("Failed to stat file %v: %v", fw.Filename, err)
   134  			}
   135  
   136  			// File got moved/renamed?
   137  			if !os.SameFile(origFi, fi) {
   138  				changes.NotifyDeleted()
   139  				return
   140  			}
   141  
   142  			// File got truncated?
   143  			fw.Size = fi.Size()
   144  			if prevSize > 0 && prevSize > fw.Size {
   145  				changes.NotifyTruncated()
   146  				prevSize = fw.Size
   147  				bo.Reset()
   148  				continue
   149  			}
   150  			// File got bigger?
   151  			if prevSize > 0 && prevSize < fw.Size {
   152  				changes.NotifyModified()
   153  				prevSize = fw.Size
   154  				bo.Reset()
   155  				continue
   156  			}
   157  			prevSize = fw.Size
   158  
   159  			// File was appended to (changed)?
   160  			modTime := fi.ModTime()
   161  			if modTime != prevModTime {
   162  				prevModTime = modTime
   163  				changes.NotifyModified()
   164  				bo.Reset()
   165  				continue
   166  			}
   167  
   168  			// File hasn't changed; increase backoff for next sleep.
   169  			bo.Backoff()
   170  		}
   171  	}()
   172  
   173  	return changes, nil
   174  }
   175  
   176  func (fw *PollingFileWatcher) SetFile(f *os.File) {
   177  	fw.File = f
   178  }
   179  
   180  func (fw *PollingFileWatcher) closeFile() {
   181  	if fw.File != nil {
   182  		_ = fw.File.Close() // Best effort close
   183  	}
   184  }
   185  
   186  type pollBackoff struct {
   187  	current time.Duration
   188  	opts    PollingFileWatcherOptions
   189  }
   190  
   191  func newPollBackoff(opts PollingFileWatcherOptions) *pollBackoff {
   192  	return &pollBackoff{
   193  		current: opts.MinPollFrequency,
   194  		opts:    opts,
   195  	}
   196  }
   197  
   198  func (pb *pollBackoff) WaitTime() time.Duration {
   199  	return pb.current
   200  }
   201  
   202  func (pb *pollBackoff) Reset() {
   203  	pb.current = pb.opts.MinPollFrequency
   204  }
   205  
   206  func (pb *pollBackoff) Backoff() {
   207  	pb.current = pb.current * 2
   208  	if pb.current > pb.opts.MaxPollFrequency {
   209  		pb.current = pb.opts.MaxPollFrequency
   210  	}
   211  }