119ea8026Sopenharmony_ci#!/usr/bin/env python3
219ea8026Sopenharmony_ci#
319ea8026Sopenharmony_ci# Efficiently displays the last n lines of a file/pipe.
419ea8026Sopenharmony_ci#
519ea8026Sopenharmony_ci# Example:
619ea8026Sopenharmony_ci# ./scripts/tailpipe.py trace -n5
719ea8026Sopenharmony_ci#
819ea8026Sopenharmony_ci# Copyright (c) 2022, The littlefs authors.
919ea8026Sopenharmony_ci# SPDX-License-Identifier: BSD-3-Clause
1019ea8026Sopenharmony_ci#
1119ea8026Sopenharmony_ci
1219ea8026Sopenharmony_ciimport collections as co
1319ea8026Sopenharmony_ciimport io
1419ea8026Sopenharmony_ciimport os
1519ea8026Sopenharmony_ciimport select
1619ea8026Sopenharmony_ciimport shutil
1719ea8026Sopenharmony_ciimport sys
1819ea8026Sopenharmony_ciimport threading as th
1919ea8026Sopenharmony_ciimport time
2019ea8026Sopenharmony_ci
2119ea8026Sopenharmony_ci
2219ea8026Sopenharmony_cidef openio(path, mode='r', buffering=-1):
2319ea8026Sopenharmony_ci    # allow '-' for stdin/stdout
2419ea8026Sopenharmony_ci    if path == '-':
2519ea8026Sopenharmony_ci        if mode == 'r':
2619ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
2719ea8026Sopenharmony_ci        else:
2819ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
2919ea8026Sopenharmony_ci    else:
3019ea8026Sopenharmony_ci        return open(path, mode, buffering)
3119ea8026Sopenharmony_ci
3219ea8026Sopenharmony_ciclass LinesIO:
3319ea8026Sopenharmony_ci    def __init__(self, maxlen=None):
3419ea8026Sopenharmony_ci        self.maxlen = maxlen
3519ea8026Sopenharmony_ci        self.lines = co.deque(maxlen=maxlen)
3619ea8026Sopenharmony_ci        self.tail = io.StringIO()
3719ea8026Sopenharmony_ci
3819ea8026Sopenharmony_ci        # trigger automatic sizing
3919ea8026Sopenharmony_ci        if maxlen == 0:
4019ea8026Sopenharmony_ci            self.resize(0)
4119ea8026Sopenharmony_ci
4219ea8026Sopenharmony_ci    def write(self, s):
4319ea8026Sopenharmony_ci        # note using split here ensures the trailing string has no newline
4419ea8026Sopenharmony_ci        lines = s.split('\n')
4519ea8026Sopenharmony_ci
4619ea8026Sopenharmony_ci        if len(lines) > 1 and self.tail.getvalue():
4719ea8026Sopenharmony_ci            self.tail.write(lines[0])
4819ea8026Sopenharmony_ci            lines[0] = self.tail.getvalue()
4919ea8026Sopenharmony_ci            self.tail = io.StringIO()
5019ea8026Sopenharmony_ci
5119ea8026Sopenharmony_ci        self.lines.extend(lines[:-1])
5219ea8026Sopenharmony_ci
5319ea8026Sopenharmony_ci        if lines[-1]:
5419ea8026Sopenharmony_ci            self.tail.write(lines[-1])
5519ea8026Sopenharmony_ci
5619ea8026Sopenharmony_ci    def resize(self, maxlen):
5719ea8026Sopenharmony_ci        self.maxlen = maxlen
5819ea8026Sopenharmony_ci        if maxlen == 0:
5919ea8026Sopenharmony_ci            maxlen = shutil.get_terminal_size((80, 5))[1]
6019ea8026Sopenharmony_ci        if maxlen != self.lines.maxlen:
6119ea8026Sopenharmony_ci            self.lines = co.deque(self.lines, maxlen=maxlen)
6219ea8026Sopenharmony_ci
6319ea8026Sopenharmony_ci    canvas_lines = 1
6419ea8026Sopenharmony_ci    def draw(self):
6519ea8026Sopenharmony_ci        # did terminal size change?
6619ea8026Sopenharmony_ci        if self.maxlen == 0:
6719ea8026Sopenharmony_ci            self.resize(0)
6819ea8026Sopenharmony_ci
6919ea8026Sopenharmony_ci        # first thing first, give ourself a canvas
7019ea8026Sopenharmony_ci        while LinesIO.canvas_lines < len(self.lines):
7119ea8026Sopenharmony_ci            sys.stdout.write('\n')
7219ea8026Sopenharmony_ci            LinesIO.canvas_lines += 1
7319ea8026Sopenharmony_ci
7419ea8026Sopenharmony_ci        # clear the bottom of the canvas if we shrink
7519ea8026Sopenharmony_ci        shrink = LinesIO.canvas_lines - len(self.lines)
7619ea8026Sopenharmony_ci        if shrink > 0:
7719ea8026Sopenharmony_ci            for i in range(shrink):
7819ea8026Sopenharmony_ci                sys.stdout.write('\r')
7919ea8026Sopenharmony_ci                if shrink-1-i > 0:
8019ea8026Sopenharmony_ci                    sys.stdout.write('\x1b[%dA' % (shrink-1-i))
8119ea8026Sopenharmony_ci                sys.stdout.write('\x1b[K')
8219ea8026Sopenharmony_ci                if shrink-1-i > 0:
8319ea8026Sopenharmony_ci                    sys.stdout.write('\x1b[%dB' % (shrink-1-i))
8419ea8026Sopenharmony_ci            sys.stdout.write('\x1b[%dA' % shrink)
8519ea8026Sopenharmony_ci            LinesIO.canvas_lines = len(self.lines)
8619ea8026Sopenharmony_ci
8719ea8026Sopenharmony_ci        for i, line in enumerate(self.lines):
8819ea8026Sopenharmony_ci            # move cursor, clear line, disable/reenable line wrapping
8919ea8026Sopenharmony_ci            sys.stdout.write('\r')
9019ea8026Sopenharmony_ci            if len(self.lines)-1-i > 0:
9119ea8026Sopenharmony_ci                sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i))
9219ea8026Sopenharmony_ci            sys.stdout.write('\x1b[K')
9319ea8026Sopenharmony_ci            sys.stdout.write('\x1b[?7l')
9419ea8026Sopenharmony_ci            sys.stdout.write(line)
9519ea8026Sopenharmony_ci            sys.stdout.write('\x1b[?7h')
9619ea8026Sopenharmony_ci            if len(self.lines)-1-i > 0:
9719ea8026Sopenharmony_ci                sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i))
9819ea8026Sopenharmony_ci        sys.stdout.flush()
9919ea8026Sopenharmony_ci
10019ea8026Sopenharmony_ci
10119ea8026Sopenharmony_cidef main(path='-', *, lines=5, cat=False, sleep=None, keep_open=False):
10219ea8026Sopenharmony_ci    if cat:
10319ea8026Sopenharmony_ci        ring = sys.stdout
10419ea8026Sopenharmony_ci    else:
10519ea8026Sopenharmony_ci        ring = LinesIO(lines)
10619ea8026Sopenharmony_ci
10719ea8026Sopenharmony_ci    # if sleep print in background thread to avoid getting stuck in a read call
10819ea8026Sopenharmony_ci    event = th.Event()
10919ea8026Sopenharmony_ci    lock = th.Lock()
11019ea8026Sopenharmony_ci    if not cat:
11119ea8026Sopenharmony_ci        done = False
11219ea8026Sopenharmony_ci        def background():
11319ea8026Sopenharmony_ci            while not done:
11419ea8026Sopenharmony_ci                event.wait()
11519ea8026Sopenharmony_ci                event.clear()
11619ea8026Sopenharmony_ci                with lock:
11719ea8026Sopenharmony_ci                    ring.draw()
11819ea8026Sopenharmony_ci                time.sleep(sleep or 0.01)
11919ea8026Sopenharmony_ci        th.Thread(target=background, daemon=True).start()
12019ea8026Sopenharmony_ci
12119ea8026Sopenharmony_ci    try:
12219ea8026Sopenharmony_ci        while True:
12319ea8026Sopenharmony_ci            with openio(path) as f:
12419ea8026Sopenharmony_ci                for line in f:
12519ea8026Sopenharmony_ci                    with lock:
12619ea8026Sopenharmony_ci                        ring.write(line)
12719ea8026Sopenharmony_ci                        event.set()
12819ea8026Sopenharmony_ci
12919ea8026Sopenharmony_ci            if not keep_open:
13019ea8026Sopenharmony_ci                break
13119ea8026Sopenharmony_ci            # don't just flood open calls
13219ea8026Sopenharmony_ci            time.sleep(sleep or 0.1)
13319ea8026Sopenharmony_ci    except FileNotFoundError as e:
13419ea8026Sopenharmony_ci        print("error: file not found %r" % path)
13519ea8026Sopenharmony_ci        sys.exit(-1)
13619ea8026Sopenharmony_ci    except KeyboardInterrupt:
13719ea8026Sopenharmony_ci        pass
13819ea8026Sopenharmony_ci
13919ea8026Sopenharmony_ci    if not cat:
14019ea8026Sopenharmony_ci        done = True
14119ea8026Sopenharmony_ci        lock.acquire() # avoids https://bugs.python.org/issue42717
14219ea8026Sopenharmony_ci        sys.stdout.write('\n')
14319ea8026Sopenharmony_ci
14419ea8026Sopenharmony_ci
14519ea8026Sopenharmony_ciif __name__ == "__main__":
14619ea8026Sopenharmony_ci    import sys
14719ea8026Sopenharmony_ci    import argparse
14819ea8026Sopenharmony_ci    parser = argparse.ArgumentParser(
14919ea8026Sopenharmony_ci        description="Efficiently displays the last n lines of a file/pipe.",
15019ea8026Sopenharmony_ci        allow_abbrev=False)
15119ea8026Sopenharmony_ci    parser.add_argument(
15219ea8026Sopenharmony_ci        'path',
15319ea8026Sopenharmony_ci        nargs='?',
15419ea8026Sopenharmony_ci        help="Path to read from.")
15519ea8026Sopenharmony_ci    parser.add_argument(
15619ea8026Sopenharmony_ci        '-n', '--lines',
15719ea8026Sopenharmony_ci        nargs='?',
15819ea8026Sopenharmony_ci        type=lambda x: int(x, 0),
15919ea8026Sopenharmony_ci        const=0,
16019ea8026Sopenharmony_ci        help="Show this many lines of history. 0 uses the terminal height. "
16119ea8026Sopenharmony_ci            "Defaults to 5.")
16219ea8026Sopenharmony_ci    parser.add_argument(
16319ea8026Sopenharmony_ci        '-z', '--cat',
16419ea8026Sopenharmony_ci        action='store_true',
16519ea8026Sopenharmony_ci        help="Pipe directly to stdout.")
16619ea8026Sopenharmony_ci    parser.add_argument(
16719ea8026Sopenharmony_ci        '-s', '--sleep',
16819ea8026Sopenharmony_ci        type=float,
16919ea8026Sopenharmony_ci        help="Seconds to sleep between reads. Defaults to 0.01.")
17019ea8026Sopenharmony_ci    parser.add_argument(
17119ea8026Sopenharmony_ci        '-k', '--keep-open',
17219ea8026Sopenharmony_ci        action='store_true',
17319ea8026Sopenharmony_ci        help="Reopen the pipe on EOF, useful when multiple "
17419ea8026Sopenharmony_ci            "processes are writing.")
17519ea8026Sopenharmony_ci    sys.exit(main(**{k: v
17619ea8026Sopenharmony_ci        for k, v in vars(parser.parse_intermixed_args()).items()
17719ea8026Sopenharmony_ci        if v is not None}))
178