119ea8026Sopenharmony_ci#!/usr/bin/env python3 219ea8026Sopenharmony_ci# 319ea8026Sopenharmony_ci# Traditional watch command, but with higher resolution updates and a bit 419ea8026Sopenharmony_ci# different options/output format 519ea8026Sopenharmony_ci# 619ea8026Sopenharmony_ci# Example: 719ea8026Sopenharmony_ci# ./scripts/watch.py -s0.1 date 819ea8026Sopenharmony_ci# 919ea8026Sopenharmony_ci# Copyright (c) 2022, The littlefs authors. 1019ea8026Sopenharmony_ci# SPDX-License-Identifier: BSD-3-Clause 1119ea8026Sopenharmony_ci# 1219ea8026Sopenharmony_ci 1319ea8026Sopenharmony_ciimport collections as co 1419ea8026Sopenharmony_ciimport errno 1519ea8026Sopenharmony_ciimport fcntl 1619ea8026Sopenharmony_ciimport io 1719ea8026Sopenharmony_ciimport os 1819ea8026Sopenharmony_ciimport pty 1919ea8026Sopenharmony_ciimport re 2019ea8026Sopenharmony_ciimport shutil 2119ea8026Sopenharmony_ciimport struct 2219ea8026Sopenharmony_ciimport subprocess as sp 2319ea8026Sopenharmony_ciimport sys 2419ea8026Sopenharmony_ciimport termios 2519ea8026Sopenharmony_ciimport time 2619ea8026Sopenharmony_ci 2719ea8026Sopenharmony_citry: 2819ea8026Sopenharmony_ci import inotify_simple 2919ea8026Sopenharmony_ciexcept ModuleNotFoundError: 3019ea8026Sopenharmony_ci inotify_simple = None 3119ea8026Sopenharmony_ci 3219ea8026Sopenharmony_ci 3319ea8026Sopenharmony_cidef openio(path, mode='r', buffering=-1): 3419ea8026Sopenharmony_ci # allow '-' for stdin/stdout 3519ea8026Sopenharmony_ci if path == '-': 3619ea8026Sopenharmony_ci if mode == 'r': 3719ea8026Sopenharmony_ci return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) 3819ea8026Sopenharmony_ci else: 3919ea8026Sopenharmony_ci return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) 4019ea8026Sopenharmony_ci else: 4119ea8026Sopenharmony_ci return open(path, mode, buffering) 4219ea8026Sopenharmony_ci 4319ea8026Sopenharmony_cidef inotifywait(paths): 4419ea8026Sopenharmony_ci # wait for interesting events 4519ea8026Sopenharmony_ci inotify = inotify_simple.INotify() 4619ea8026Sopenharmony_ci flags = (inotify_simple.flags.ATTRIB 4719ea8026Sopenharmony_ci | inotify_simple.flags.CREATE 4819ea8026Sopenharmony_ci | inotify_simple.flags.DELETE 4919ea8026Sopenharmony_ci | inotify_simple.flags.DELETE_SELF 5019ea8026Sopenharmony_ci | inotify_simple.flags.MODIFY 5119ea8026Sopenharmony_ci | inotify_simple.flags.MOVED_FROM 5219ea8026Sopenharmony_ci | inotify_simple.flags.MOVED_TO 5319ea8026Sopenharmony_ci | inotify_simple.flags.MOVE_SELF) 5419ea8026Sopenharmony_ci 5519ea8026Sopenharmony_ci # recurse into directories 5619ea8026Sopenharmony_ci for path in paths: 5719ea8026Sopenharmony_ci if os.path.isdir(path): 5819ea8026Sopenharmony_ci for dir, _, files in os.walk(path): 5919ea8026Sopenharmony_ci inotify.add_watch(dir, flags) 6019ea8026Sopenharmony_ci for f in files: 6119ea8026Sopenharmony_ci inotify.add_watch(os.path.join(dir, f), flags) 6219ea8026Sopenharmony_ci else: 6319ea8026Sopenharmony_ci inotify.add_watch(path, flags) 6419ea8026Sopenharmony_ci 6519ea8026Sopenharmony_ci # wait for event 6619ea8026Sopenharmony_ci inotify.read() 6719ea8026Sopenharmony_ci 6819ea8026Sopenharmony_ciclass LinesIO: 6919ea8026Sopenharmony_ci def __init__(self, maxlen=None): 7019ea8026Sopenharmony_ci self.maxlen = maxlen 7119ea8026Sopenharmony_ci self.lines = co.deque(maxlen=maxlen) 7219ea8026Sopenharmony_ci self.tail = io.StringIO() 7319ea8026Sopenharmony_ci 7419ea8026Sopenharmony_ci # trigger automatic sizing 7519ea8026Sopenharmony_ci if maxlen == 0: 7619ea8026Sopenharmony_ci self.resize(0) 7719ea8026Sopenharmony_ci 7819ea8026Sopenharmony_ci def write(self, s): 7919ea8026Sopenharmony_ci # note using split here ensures the trailing string has no newline 8019ea8026Sopenharmony_ci lines = s.split('\n') 8119ea8026Sopenharmony_ci 8219ea8026Sopenharmony_ci if len(lines) > 1 and self.tail.getvalue(): 8319ea8026Sopenharmony_ci self.tail.write(lines[0]) 8419ea8026Sopenharmony_ci lines[0] = self.tail.getvalue() 8519ea8026Sopenharmony_ci self.tail = io.StringIO() 8619ea8026Sopenharmony_ci 8719ea8026Sopenharmony_ci self.lines.extend(lines[:-1]) 8819ea8026Sopenharmony_ci 8919ea8026Sopenharmony_ci if lines[-1]: 9019ea8026Sopenharmony_ci self.tail.write(lines[-1]) 9119ea8026Sopenharmony_ci 9219ea8026Sopenharmony_ci def resize(self, maxlen): 9319ea8026Sopenharmony_ci self.maxlen = maxlen 9419ea8026Sopenharmony_ci if maxlen == 0: 9519ea8026Sopenharmony_ci maxlen = shutil.get_terminal_size((80, 5))[1] 9619ea8026Sopenharmony_ci if maxlen != self.lines.maxlen: 9719ea8026Sopenharmony_ci self.lines = co.deque(self.lines, maxlen=maxlen) 9819ea8026Sopenharmony_ci 9919ea8026Sopenharmony_ci canvas_lines = 1 10019ea8026Sopenharmony_ci def draw(self): 10119ea8026Sopenharmony_ci # did terminal size change? 10219ea8026Sopenharmony_ci if self.maxlen == 0: 10319ea8026Sopenharmony_ci self.resize(0) 10419ea8026Sopenharmony_ci 10519ea8026Sopenharmony_ci # first thing first, give ourself a canvas 10619ea8026Sopenharmony_ci while LinesIO.canvas_lines < len(self.lines): 10719ea8026Sopenharmony_ci sys.stdout.write('\n') 10819ea8026Sopenharmony_ci LinesIO.canvas_lines += 1 10919ea8026Sopenharmony_ci 11019ea8026Sopenharmony_ci # clear the bottom of the canvas if we shrink 11119ea8026Sopenharmony_ci shrink = LinesIO.canvas_lines - len(self.lines) 11219ea8026Sopenharmony_ci if shrink > 0: 11319ea8026Sopenharmony_ci for i in range(shrink): 11419ea8026Sopenharmony_ci sys.stdout.write('\r') 11519ea8026Sopenharmony_ci if shrink-1-i > 0: 11619ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dA' % (shrink-1-i)) 11719ea8026Sopenharmony_ci sys.stdout.write('\x1b[K') 11819ea8026Sopenharmony_ci if shrink-1-i > 0: 11919ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dB' % (shrink-1-i)) 12019ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dA' % shrink) 12119ea8026Sopenharmony_ci LinesIO.canvas_lines = len(self.lines) 12219ea8026Sopenharmony_ci 12319ea8026Sopenharmony_ci for i, line in enumerate(self.lines): 12419ea8026Sopenharmony_ci # move cursor, clear line, disable/reenable line wrapping 12519ea8026Sopenharmony_ci sys.stdout.write('\r') 12619ea8026Sopenharmony_ci if len(self.lines)-1-i > 0: 12719ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i)) 12819ea8026Sopenharmony_ci sys.stdout.write('\x1b[K') 12919ea8026Sopenharmony_ci sys.stdout.write('\x1b[?7l') 13019ea8026Sopenharmony_ci sys.stdout.write(line) 13119ea8026Sopenharmony_ci sys.stdout.write('\x1b[?7h') 13219ea8026Sopenharmony_ci if len(self.lines)-1-i > 0: 13319ea8026Sopenharmony_ci sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i)) 13419ea8026Sopenharmony_ci sys.stdout.flush() 13519ea8026Sopenharmony_ci 13619ea8026Sopenharmony_ci 13719ea8026Sopenharmony_cidef main(command, *, 13819ea8026Sopenharmony_ci lines=0, 13919ea8026Sopenharmony_ci cat=False, 14019ea8026Sopenharmony_ci sleep=None, 14119ea8026Sopenharmony_ci keep_open=False, 14219ea8026Sopenharmony_ci keep_open_paths=None, 14319ea8026Sopenharmony_ci exit_on_error=False): 14419ea8026Sopenharmony_ci returncode = 0 14519ea8026Sopenharmony_ci try: 14619ea8026Sopenharmony_ci while True: 14719ea8026Sopenharmony_ci # reset ring each run 14819ea8026Sopenharmony_ci if cat: 14919ea8026Sopenharmony_ci ring = sys.stdout 15019ea8026Sopenharmony_ci else: 15119ea8026Sopenharmony_ci ring = LinesIO(lines) 15219ea8026Sopenharmony_ci 15319ea8026Sopenharmony_ci try: 15419ea8026Sopenharmony_ci # run the command under a pseudoterminal 15519ea8026Sopenharmony_ci mpty, spty = pty.openpty() 15619ea8026Sopenharmony_ci 15719ea8026Sopenharmony_ci # forward terminal size 15819ea8026Sopenharmony_ci w, h = shutil.get_terminal_size((80, 5)) 15919ea8026Sopenharmony_ci if lines: 16019ea8026Sopenharmony_ci h = lines 16119ea8026Sopenharmony_ci fcntl.ioctl(spty, termios.TIOCSWINSZ, 16219ea8026Sopenharmony_ci struct.pack('HHHH', h, w, 0, 0)) 16319ea8026Sopenharmony_ci 16419ea8026Sopenharmony_ci proc = sp.Popen(command, 16519ea8026Sopenharmony_ci stdout=spty, 16619ea8026Sopenharmony_ci stderr=spty, 16719ea8026Sopenharmony_ci close_fds=False) 16819ea8026Sopenharmony_ci os.close(spty) 16919ea8026Sopenharmony_ci mpty = os.fdopen(mpty, 'r', 1) 17019ea8026Sopenharmony_ci 17119ea8026Sopenharmony_ci while True: 17219ea8026Sopenharmony_ci try: 17319ea8026Sopenharmony_ci line = mpty.readline() 17419ea8026Sopenharmony_ci except OSError as e: 17519ea8026Sopenharmony_ci if e.errno != errno.EIO: 17619ea8026Sopenharmony_ci raise 17719ea8026Sopenharmony_ci break 17819ea8026Sopenharmony_ci if not line: 17919ea8026Sopenharmony_ci break 18019ea8026Sopenharmony_ci 18119ea8026Sopenharmony_ci ring.write(line) 18219ea8026Sopenharmony_ci if not cat: 18319ea8026Sopenharmony_ci ring.draw() 18419ea8026Sopenharmony_ci 18519ea8026Sopenharmony_ci mpty.close() 18619ea8026Sopenharmony_ci proc.wait() 18719ea8026Sopenharmony_ci if exit_on_error and proc.returncode != 0: 18819ea8026Sopenharmony_ci returncode = proc.returncode 18919ea8026Sopenharmony_ci break 19019ea8026Sopenharmony_ci except OSError as e: 19119ea8026Sopenharmony_ci if e.errno != errno.ETXTBSY: 19219ea8026Sopenharmony_ci raise 19319ea8026Sopenharmony_ci pass 19419ea8026Sopenharmony_ci 19519ea8026Sopenharmony_ci # try to inotifywait 19619ea8026Sopenharmony_ci if keep_open and inotify_simple is not None: 19719ea8026Sopenharmony_ci if keep_open_paths: 19819ea8026Sopenharmony_ci paths = set(keep_paths) 19919ea8026Sopenharmony_ci else: 20019ea8026Sopenharmony_ci # guess inotify paths from command 20119ea8026Sopenharmony_ci paths = set() 20219ea8026Sopenharmony_ci for p in command: 20319ea8026Sopenharmony_ci for p in { 20419ea8026Sopenharmony_ci p, 20519ea8026Sopenharmony_ci re.sub('^-.', '', p), 20619ea8026Sopenharmony_ci re.sub('^--[^=]+=', '', p)}: 20719ea8026Sopenharmony_ci if p and os.path.exists(p): 20819ea8026Sopenharmony_ci paths.add(p) 20919ea8026Sopenharmony_ci ptime = time.time() 21019ea8026Sopenharmony_ci inotifywait(paths) 21119ea8026Sopenharmony_ci # sleep for a minimum amount of time, this helps issues around 21219ea8026Sopenharmony_ci # rapidly updating files 21319ea8026Sopenharmony_ci time.sleep(max(0, (sleep or 0.1) - (time.time()-ptime))) 21419ea8026Sopenharmony_ci else: 21519ea8026Sopenharmony_ci time.sleep(sleep or 0.1) 21619ea8026Sopenharmony_ci except KeyboardInterrupt: 21719ea8026Sopenharmony_ci pass 21819ea8026Sopenharmony_ci 21919ea8026Sopenharmony_ci if not cat: 22019ea8026Sopenharmony_ci sys.stdout.write('\n') 22119ea8026Sopenharmony_ci sys.exit(returncode) 22219ea8026Sopenharmony_ci 22319ea8026Sopenharmony_ci 22419ea8026Sopenharmony_ciif __name__ == "__main__": 22519ea8026Sopenharmony_ci import sys 22619ea8026Sopenharmony_ci import argparse 22719ea8026Sopenharmony_ci parser = argparse.ArgumentParser( 22819ea8026Sopenharmony_ci description="Traditional watch command, but with higher resolution " 22919ea8026Sopenharmony_ci "updates and a bit different options/output format.", 23019ea8026Sopenharmony_ci allow_abbrev=False) 23119ea8026Sopenharmony_ci parser.add_argument( 23219ea8026Sopenharmony_ci 'command', 23319ea8026Sopenharmony_ci nargs=argparse.REMAINDER, 23419ea8026Sopenharmony_ci help="Command to run.") 23519ea8026Sopenharmony_ci parser.add_argument( 23619ea8026Sopenharmony_ci '-n', '--lines', 23719ea8026Sopenharmony_ci nargs='?', 23819ea8026Sopenharmony_ci type=lambda x: int(x, 0), 23919ea8026Sopenharmony_ci const=0, 24019ea8026Sopenharmony_ci help="Show this many lines of history. 0 uses the terminal height. " 24119ea8026Sopenharmony_ci "Defaults to 0.") 24219ea8026Sopenharmony_ci parser.add_argument( 24319ea8026Sopenharmony_ci '-z', '--cat', 24419ea8026Sopenharmony_ci action='store_true', 24519ea8026Sopenharmony_ci help="Pipe directly to stdout.") 24619ea8026Sopenharmony_ci parser.add_argument( 24719ea8026Sopenharmony_ci '-s', '--sleep', 24819ea8026Sopenharmony_ci type=float, 24919ea8026Sopenharmony_ci help="Seconds to sleep between runs. Defaults to 0.1.") 25019ea8026Sopenharmony_ci parser.add_argument( 25119ea8026Sopenharmony_ci '-k', '--keep-open', 25219ea8026Sopenharmony_ci action='store_true', 25319ea8026Sopenharmony_ci help="Try to use inotify to wait for changes.") 25419ea8026Sopenharmony_ci parser.add_argument( 25519ea8026Sopenharmony_ci '-K', '--keep-open-path', 25619ea8026Sopenharmony_ci dest='keep_open_paths', 25719ea8026Sopenharmony_ci action='append', 25819ea8026Sopenharmony_ci help="Use this path for inotify. Defaults to guessing.") 25919ea8026Sopenharmony_ci parser.add_argument( 26019ea8026Sopenharmony_ci '-e', '--exit-on-error', 26119ea8026Sopenharmony_ci action='store_true', 26219ea8026Sopenharmony_ci help="Exit on error.") 26319ea8026Sopenharmony_ci sys.exit(main(**{k: v 26419ea8026Sopenharmony_ci for k, v in vars(parser.parse_args()).items() 26519ea8026Sopenharmony_ci if v is not None})) 266