#!/usr/bin/env python2

# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Tails a file, and quits when inotify detects that it has been closed."""

from __future__ import print_function

import argparse
import select
import subprocess
import sys
import time
import contextlib


@contextlib.contextmanager
def WriterClosedFile(path):
    """Context manager to watch whether a file is closed by a writer.

    @param path: the path to watch.
    """
    inotify_process = subprocess.Popen(
        ['inotifywait', '-qe', 'close_write', path],
        stdout=subprocess.PIPE)

    # stdout.read is blocking, so use select.select to detect if input is
    # available.
    def IsClosed():
        """Returns whether the inotify_process.stdout file is closed."""
        read_list, _, _ = select.select([inotify_process.stdout], [], [], 0)
        return bool(read_list)

    try:
        yield IsClosed
    finally:
        inotify_process.kill()


def TailFile(path, sleep_interval, chunk_size,
             outfile=sys.stdout,
             seek_to_end=True):
    """Tails a file, and quits when there are no writers on the file.

    @param path: The path to the file to open
    @param sleep_interval: The amount to sleep in between reads to reduce
                           wasted IO
    @param chunk_size: The amount of bytes to read in between print() calls
    @param outfile: A file handle to write to.  Defaults to sys.stdout
    @param seek_to_end: Whether to start at the end of the file at |path| when
                        reading.
    """

    def ReadChunks(fh):
        """Reads all chunks from a file handle, and prints them to |outfile|.

        @param fh: The filehandle to read from.
        """
        for chunk in iter(lambda: fh.read(chunk_size), b''):
            print(chunk, end='', file=outfile)
            outfile.flush()

    with WriterClosedFile(path) as IsClosed:
        with open(path) as fh:
            if seek_to_end == True:
                fh.seek(0, 2)
            while True:
                ReadChunks(fh)
                if IsClosed():
                    # We need to read the chunks again to avoid a race condition
                    # where the writer finishes writing some output in between
                    # the ReadChunks() and the IsClosed() call.
                    ReadChunks(fh)
                    break

                # Sleep a bit to limit the number of wasted reads.
                time.sleep(sleep_interval)


def Main():
    """Main entrypoint for the script."""
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('file', help='The file to tail')
    p.add_argument('--sleep_interval', type=float, default=0.1,
                   help='Time sleeping between file reads')
    p.add_argument('--chunk_size', type=int, default=64 * 2**10,
                   help='Bytes to read before yielding')
    p.add_argument('--from_beginning', action='store_true',
                   help='If given, read from the beginning of the file.')
    args = p.parse_args()

    TailFile(args.file, args.sleep_interval, args.chunk_size,
             seek_to_end=not args.from_beginning)


if __name__ == '__main__':
    Main()