blob: d7e275adbb59198d11c924327191bed126606b20 [file] [log] [blame]
Paul Hobbsbe84dd12016-07-01 11:54:37 -07001#!/usr/bin/python2
2
3# Copyright 2016 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
Paul Hobbsee5a8c32016-07-08 14:45:18 -070016import contextlib
17
18
19@contextlib.contextmanager
20def WriterClosedFile(path):
21 """Context manager to watch whether a file is closed by a writer."""
22 inotify_process = subprocess.Popen(
23 ['inotifywait', '-qe', 'close_write', path],
24 stdout=subprocess.PIPE)
25
26 # stdout.read is blocking, so use select.select to detect if input is
27 # available.
28 def IsClosed():
29 read_list, _, _ = select.select([inotify_process.stdout], [], [], 0)
30 return bool(read_list)
31
32 try:
33 yield IsClosed
34 finally:
35 inotify_process.kill()
Paul Hobbsbe84dd12016-07-01 11:54:37 -070036
37
38def TailFile(path, sleep_interval, chunk_size,
39 outfile=sys.stdout,
40 seek_to_end=True):
41 """Tails a file, and quits when there are no writers on the file.
42
43 Args:
44 path: The path to the file to open
45 sleep_interval: The amount to sleep in between reads to reduce wasted IO
46 chunk_size: The amount of bytes to read in between print() calls
47 outfile: A file handle to write to. Defaults to sys.stdout
48 seek_to_end: Whether to start at the end of the file at |path| when reading.
49 """
50
Paul Hobbsbe84dd12016-07-01 11:54:37 -070051 def ReadChunks(fh):
52 for chunk in iter(lambda: fh.read(chunk_size), b''):
53 print(chunk, end='', file=outfile)
54
Paul Hobbsee5a8c32016-07-08 14:45:18 -070055 with WriterClosedFile(path) as IsClosed:
56 with open(path) as fh:
57 if seek_to_end == True:
58 fh.seek(0, 2)
59 while True:
Paul Hobbsbe84dd12016-07-01 11:54:37 -070060 ReadChunks(fh)
Paul Hobbsee5a8c32016-07-08 14:45:18 -070061 if IsClosed():
62 # We need to read the chunks again to avoid a race condition where the
63 # writer finishes writing some output in between the ReadChunks() and
64 # the IsClosed() call.
65 ReadChunks(fh)
66 break
Paul Hobbsbe84dd12016-07-01 11:54:37 -070067
Paul Hobbsee5a8c32016-07-08 14:45:18 -070068 # Sleep a bit to limit the number of wasted reads.
69 time.sleep(sleep_interval)
Paul Hobbsbe84dd12016-07-01 11:54:37 -070070
71
72def main():
73 p = argparse.ArgumentParser(description=__doc__)
74 p.add_argument('file', help='The file to tail')
75 p.add_argument('--sleep_interval', type=float, default=0.1,
76 help='Time sleeping between file reads')
77 p.add_argument('--chunk_size', type=int, default=64 * 2**10,
78 help='Bytes to read before yielding')
79 p.add_argument('--from_beginning', action='store_true',
80 help='If given, read from the beginning of the file.')
81 args = p.parse_args()
82
83 TailFile(args.file, args.sleep_interval, args.chunk_size,
84 seek_to_end=not args.from_beginning)
85
86
87if __name__ == '__main__':
88 main()