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  }