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 }