Home | History | Annotate | Download | only in stats
      1 #!/usr/bin/env python2
      2 
      3 # Copyright 2017 The Chromium OS Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 """Tails a file, and quits when inotify detects that it has been closed."""
      8 
      9 from __future__ import print_function
     10 
     11 import argparse
     12 import select
     13 import subprocess
     14 import sys
     15 import time
     16 import contextlib
     17 
     18 
     19 @contextlib.contextmanager
     20 def WriterClosedFile(path):
     21     """Context manager to watch whether a file is closed by a writer.
     22 
     23     @param path: the path to watch.
     24     """
     25     inotify_process = subprocess.Popen(
     26         ['inotifywait', '-qe', 'close_write', path],
     27         stdout=subprocess.PIPE)
     28 
     29     # stdout.read is blocking, so use select.select to detect if input is
     30     # available.
     31     def IsClosed():
     32         """Returns whether the inotify_process.stdout file is closed."""
     33         read_list, _, _ = select.select([inotify_process.stdout], [], [], 0)
     34         return bool(read_list)
     35 
     36     try:
     37         yield IsClosed
     38     finally:
     39         inotify_process.kill()
     40 
     41 
     42 def TailFile(path, sleep_interval, chunk_size,
     43              outfile=sys.stdout,
     44              seek_to_end=True):
     45     """Tails a file, and quits when there are no writers on the file.
     46 
     47     @param path: The path to the file to open
     48     @param sleep_interval: The amount to sleep in between reads to reduce
     49                            wasted IO
     50     @param chunk_size: The amount of bytes to read in between print() calls
     51     @param outfile: A file handle to write to.  Defaults to sys.stdout
     52     @param seek_to_end: Whether to start at the end of the file at |path| when
     53                         reading.
     54     """
     55 
     56     def ReadChunks(fh):
     57         """Reads all chunks from a file handle, and prints them to |outfile|.
     58 
     59         @param fh: The filehandle to read from.
     60         """
     61         for chunk in iter(lambda: fh.read(chunk_size), b''):
     62             print(chunk, end='', file=outfile)
     63             outfile.flush()
     64 
     65     with WriterClosedFile(path) as IsClosed:
     66         with open(path) as fh:
     67             if seek_to_end == True:
     68                 fh.seek(0, 2)
     69             while True:
     70                 ReadChunks(fh)
     71                 if IsClosed():
     72                     # We need to read the chunks again to avoid a race condition
     73                     # where the writer finishes writing some output in between
     74                     # the ReadChunks() and the IsClosed() call.
     75                     ReadChunks(fh)
     76                     break
     77 
     78                 # Sleep a bit to limit the number of wasted reads.
     79                 time.sleep(sleep_interval)
     80 
     81 
     82 def Main():
     83     """Main entrypoint for the script."""
     84     p = argparse.ArgumentParser(description=__doc__)
     85     p.add_argument('file', help='The file to tail')
     86     p.add_argument('--sleep_interval', type=float, default=0.1,
     87                    help='Time sleeping between file reads')
     88     p.add_argument('--chunk_size', type=int, default=64 * 2**10,
     89                    help='Bytes to read before yielding')
     90     p.add_argument('--from_beginning', action='store_true',
     91                    help='If given, read from the beginning of the file.')
     92     args = p.parse_args()
     93 
     94     TailFile(args.file, args.sleep_interval, args.chunk_size,
     95              seek_to_end=not args.from_beginning)
     96 
     97 
     98 if __name__ == '__main__':
     99     Main()
    100