github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/utils/tailer/tailer.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package tailer 5 6 import ( 7 "bufio" 8 "bytes" 9 "io" 10 "os" 11 "time" 12 13 "launchpad.net/tomb" 14 ) 15 16 const ( 17 bufferSize = 4096 18 polltime = time.Second 19 delimiter = '\n' 20 ) 21 22 var ( 23 delimiters = []byte{delimiter} 24 ) 25 26 // TailerFilterFunc decides if a line shall be tailed (func is nil or 27 // returns true) of shall be omitted (func returns false). 28 type TailerFilterFunc func(line []byte) bool 29 30 // Tailer reads an input line by line an tails them into the passed Writer. 31 // The lines have to be terminated with a newline. 32 type Tailer struct { 33 tomb tomb.Tomb 34 readSeeker io.ReadSeeker 35 reader *bufio.Reader 36 writeCloser io.WriteCloser 37 writer *bufio.Writer 38 lines int 39 filter TailerFilterFunc 40 bufferSize int 41 polltime time.Duration 42 } 43 44 // NewTailer starts a Tailer which reads strings from the passed 45 // ReadSeeker line by line. If a filter function is specified the read 46 // lines are filtered. The matching lines are written to the passed 47 // Writer. The reading begins the specified number of matching lines 48 // from the end. 49 func NewTailer(readSeeker io.ReadSeeker, writer io.Writer, lines int, filter TailerFilterFunc) *Tailer { 50 return newTailer(readSeeker, writer, lines, filter, bufferSize, polltime) 51 } 52 53 // newTailer starts a Tailer like NewTailer but allows the setting of 54 // the read buffer size and the time between pollings for testing. 55 func newTailer(readSeeker io.ReadSeeker, writer io.Writer, lines int, filter TailerFilterFunc, 56 bufferSize int, polltime time.Duration) *Tailer { 57 t := &Tailer{ 58 readSeeker: readSeeker, 59 reader: bufio.NewReaderSize(readSeeker, bufferSize), 60 writer: bufio.NewWriter(writer), 61 lines: lines, 62 filter: filter, 63 bufferSize: bufferSize, 64 polltime: polltime, 65 } 66 go func() { 67 defer t.tomb.Done() 68 t.tomb.Kill(t.loop()) 69 }() 70 return t 71 } 72 73 // Stop tells the tailer to stop working. 74 func (t *Tailer) Stop() error { 75 t.tomb.Kill(nil) 76 return t.tomb.Wait() 77 } 78 79 // Wait waits until the tailer is stopped due to command 80 // or an error. In case of an error it returns the reason. 81 func (t *Tailer) Wait() error { 82 return t.tomb.Wait() 83 } 84 85 // Dead returns the channel that can be used to wait until 86 // the tailer is stopped. 87 func (t *Tailer) Dead() <-chan struct{} { 88 return t.tomb.Dead() 89 } 90 91 // Err returns a possible error. 92 func (t *Tailer) Err() error { 93 return t.tomb.Err() 94 } 95 96 // loop writes the last lines based on the buffer size to the 97 // writer and then polls for more data to write it to the 98 // writer too. 99 func (t *Tailer) loop() error { 100 // Position the readSeeker. 101 if err := t.seekLastLines(); err != nil { 102 return err 103 } 104 // Start polling. 105 // TODO(mue) 2013-12-06 106 // Handling of read-seeker/files being truncated during 107 // tailing is currently missing! 108 timer := time.NewTimer(0) 109 for { 110 select { 111 case <-t.tomb.Dying(): 112 return nil 113 case <-timer.C: 114 for { 115 line, readErr := t.readLine() 116 _, writeErr := t.writer.Write(line) 117 if writeErr != nil { 118 return writeErr 119 } 120 if readErr != nil { 121 if readErr != io.EOF { 122 return readErr 123 } 124 break 125 } 126 } 127 if writeErr := t.writer.Flush(); writeErr != nil { 128 return writeErr 129 } 130 timer.Reset(t.polltime) 131 } 132 } 133 } 134 135 // seekLastLines sets the read position of the ReadSeeker to the 136 // wanted number of filtered lines before the end. 137 func (t *Tailer) seekLastLines() error { 138 offset, err := t.readSeeker.Seek(0, os.SEEK_END) 139 if err != nil { 140 return err 141 } 142 seekPos := int64(0) 143 found := 0 144 buffer := make([]byte, t.bufferSize) 145 SeekLoop: 146 for offset > 0 { 147 // buffer contains the data left over from the 148 // previous iteration. 149 space := cap(buffer) - len(buffer) 150 if space < t.bufferSize { 151 // Grow buffer. 152 newBuffer := make([]byte, len(buffer), cap(buffer)*2) 153 copy(newBuffer, buffer) 154 buffer = newBuffer 155 space = cap(buffer) - len(buffer) 156 } 157 if int64(space) > offset { 158 // Use exactly the right amount of space if there's 159 // only a small amount remaining. 160 space = int(offset) 161 } 162 // Copy data remaining from last time to the end of the buffer, 163 // so we can read into the right place. 164 copy(buffer[space:cap(buffer)], buffer) 165 buffer = buffer[0 : len(buffer)+space] 166 offset -= int64(space) 167 _, err := t.readSeeker.Seek(offset, os.SEEK_SET) 168 if err != nil { 169 return err 170 } 171 _, err = io.ReadFull(t.readSeeker, buffer[0:space]) 172 if err != nil { 173 return err 174 } 175 // Find the end of the last line in the buffer. 176 // This will discard any unterminated line at the end 177 // of the file. 178 end := bytes.LastIndex(buffer, delimiters) 179 if end == -1 { 180 // No end of line found - discard incomplete 181 // line and continue looking. If this happens 182 // at the beginning of the file, we don't care 183 // because we're going to stop anyway. 184 buffer = buffer[:0] 185 continue 186 } 187 end++ 188 for { 189 start := bytes.LastIndex(buffer[0:end-1], delimiters) 190 if start == -1 && offset >= 0 { 191 break 192 } 193 start++ 194 if t.isValid(buffer[start:end]) { 195 found++ 196 if found >= t.lines { 197 seekPos = offset + int64(start) 198 break SeekLoop 199 } 200 } 201 end = start 202 } 203 // Leave the last line in buffer, as we don't know whether 204 // it's complete or not. 205 buffer = buffer[0:end] 206 } 207 // Final positioning. 208 t.readSeeker.Seek(seekPos, os.SEEK_SET) 209 return nil 210 } 211 212 // readLine reads the next valid line from the reader, even if it is 213 // larger than the reader buffer. 214 func (t *Tailer) readLine() ([]byte, error) { 215 for { 216 slice, err := t.reader.ReadSlice(delimiter) 217 if err == nil { 218 if t.isValid(slice) { 219 return slice, nil 220 } 221 continue 222 } 223 line := append([]byte(nil), slice...) 224 for err == bufio.ErrBufferFull { 225 slice, err = t.reader.ReadSlice(delimiter) 226 line = append(line, slice...) 227 } 228 switch err { 229 case nil: 230 if t.isValid(line) { 231 return line, nil 232 } 233 case io.EOF: 234 // EOF without delimiter, step back. 235 t.readSeeker.Seek(-int64(len(line)), os.SEEK_CUR) 236 return nil, err 237 default: 238 return nil, err 239 } 240 } 241 } 242 243 // isValid checks if the passed line is valid by checking if the 244 // line has content, the filter function is nil or it returns true. 245 func (t *Tailer) isValid(line []byte) bool { 246 if t.filter == nil { 247 return true 248 } 249 return t.filter(line) 250 }