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 9from __future__ import print_function 10 11import argparse 12import select 13import subprocess 14import sys 15import time 16import contextlib 17 18 19@contextlib.contextmanager 20def 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 42def 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 82def 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 98if __name__ == '__main__': 99 Main() 100