• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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