1#!/usr/bin/env python3 2# 3# Display operations on block devices based on trace output 4# 5# Example: 6# ./scripts/tracebd.py trace 7# 8# Copyright (c) 2022, The littlefs authors. 9# SPDX-License-Identifier: BSD-3-Clause 10# 11 12import collections as co 13import functools as ft 14import io 15import itertools as it 16import math as m 17import os 18import re 19import shutil 20import threading as th 21import time 22 23 24CHARS = 'rpe.' 25COLORS = ['42', '45', '44', ''] 26 27WEAR_CHARS = '0123456789' 28WEAR_CHARS_SUBSCRIPTS = '.₁₂₃₄₅₆789' 29WEAR_COLORS = ['', '', '', '', '', '', '', '35', '35', '1;31'] 30 31CHARS_DOTS = " .':" 32COLORS_DOTS = ['32', '35', '34', ''] 33CHARS_BRAILLE = ( 34 '⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴' 35 '⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶' 36 '⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼' 37 '⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾' 38 '⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵' 39 '⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷' 40 '⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽' 41 '⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿') 42 43 44def openio(path, mode='r', buffering=-1): 45 # allow '-' for stdin/stdout 46 if path == '-': 47 if mode == 'r': 48 return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) 49 else: 50 return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) 51 else: 52 return open(path, mode, buffering) 53 54class LinesIO: 55 def __init__(self, maxlen=None): 56 self.maxlen = maxlen 57 self.lines = co.deque(maxlen=maxlen) 58 self.tail = io.StringIO() 59 60 # trigger automatic sizing 61 if maxlen == 0: 62 self.resize(0) 63 64 def write(self, s): 65 # note using split here ensures the trailing string has no newline 66 lines = s.split('\n') 67 68 if len(lines) > 1 and self.tail.getvalue(): 69 self.tail.write(lines[0]) 70 lines[0] = self.tail.getvalue() 71 self.tail = io.StringIO() 72 73 self.lines.extend(lines[:-1]) 74 75 if lines[-1]: 76 self.tail.write(lines[-1]) 77 78 def resize(self, maxlen): 79 self.maxlen = maxlen 80 if maxlen == 0: 81 maxlen = shutil.get_terminal_size((80, 5))[1] 82 if maxlen != self.lines.maxlen: 83 self.lines = co.deque(self.lines, maxlen=maxlen) 84 85 canvas_lines = 1 86 def draw(self): 87 # did terminal size change? 88 if self.maxlen == 0: 89 self.resize(0) 90 91 # first thing first, give ourself a canvas 92 while LinesIO.canvas_lines < len(self.lines): 93 sys.stdout.write('\n') 94 LinesIO.canvas_lines += 1 95 96 # clear the bottom of the canvas if we shrink 97 shrink = LinesIO.canvas_lines - len(self.lines) 98 if shrink > 0: 99 for i in range(shrink): 100 sys.stdout.write('\r') 101 if shrink-1-i > 0: 102 sys.stdout.write('\x1b[%dA' % (shrink-1-i)) 103 sys.stdout.write('\x1b[K') 104 if shrink-1-i > 0: 105 sys.stdout.write('\x1b[%dB' % (shrink-1-i)) 106 sys.stdout.write('\x1b[%dA' % shrink) 107 LinesIO.canvas_lines = len(self.lines) 108 109 for i, line in enumerate(self.lines): 110 # move cursor, clear line, disable/reenable line wrapping 111 sys.stdout.write('\r') 112 if len(self.lines)-1-i > 0: 113 sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i)) 114 sys.stdout.write('\x1b[K') 115 sys.stdout.write('\x1b[?7l') 116 sys.stdout.write(line) 117 sys.stdout.write('\x1b[?7h') 118 if len(self.lines)-1-i > 0: 119 sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i)) 120 sys.stdout.flush() 121 122 123# space filling Hilbert-curve 124# 125# note we memoize the last curve since this is a bit expensive 126# 127@ft.lru_cache(1) 128def hilbert_curve(width, height): 129 # based on generalized Hilbert curves: 130 # https://github.com/jakubcerveny/gilbert 131 # 132 def hilbert_(x, y, a_x, a_y, b_x, b_y): 133 w = abs(a_x+a_y) 134 h = abs(b_x+b_y) 135 a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0 136 a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0 137 b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0 138 b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0 139 140 # trivial row 141 if h == 1: 142 for _ in range(w): 143 yield (x,y) 144 x, y = x+a_dx, y+a_dy 145 return 146 147 # trivial column 148 if w == 1: 149 for _ in range(h): 150 yield (x,y) 151 x, y = x+b_dx, y+b_dy 152 return 153 154 a_x_, a_y_ = a_x//2, a_y//2 155 b_x_, b_y_ = b_x//2, b_y//2 156 w_ = abs(a_x_+a_y_) 157 h_ = abs(b_x_+b_y_) 158 159 if 2*w > 3*h: 160 # prefer even steps 161 if w_ % 2 != 0 and w > 2: 162 a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy 163 164 # split in two 165 yield from hilbert_(x, y, a_x_, a_y_, b_x, b_y) 166 yield from hilbert_(x+a_x_, y+a_y_, a_x-a_x_, a_y-a_y_, b_x, b_y) 167 else: 168 # prefer even steps 169 if h_ % 2 != 0 and h > 2: 170 b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy 171 172 # split in three 173 yield from hilbert_(x, y, b_x_, b_y_, a_x_, a_y_) 174 yield from hilbert_(x+b_x_, y+b_y_, a_x, a_y, b_x-b_x_, b_y-b_y_) 175 yield from hilbert_( 176 x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy), 177 -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_)) 178 179 if width >= height: 180 curve = hilbert_(0, 0, +width, 0, 0, +height) 181 else: 182 curve = hilbert_(0, 0, 0, +height, +width, 0) 183 184 return list(curve) 185 186# space filling Z-curve/Lebesgue-curve 187# 188# note we memoize the last curve since this is a bit expensive 189# 190@ft.lru_cache(1) 191def lebesgue_curve(width, height): 192 # we create a truncated Z-curve by simply filtering out the points 193 # that are outside our region 194 curve = [] 195 for i in range(2**(2*m.ceil(m.log2(max(width, height))))): 196 # we just operate on binary strings here because it's easier 197 b = '{:0{}b}'.format(i, 2*m.ceil(m.log2(i+1)/2)) 198 x = int(b[1::2], 2) if b[1::2] else 0 199 y = int(b[0::2], 2) if b[0::2] else 0 200 if x < width and y < height: 201 curve.append((x, y)) 202 203 return curve 204 205 206class Block(int): 207 __slots__ = () 208 def __new__(cls, state=0, *, 209 wear=0, 210 readed=False, 211 proged=False, 212 erased=False): 213 return super().__new__(cls, 214 state 215 | (wear << 3) 216 | (1 if readed else 0) 217 | (2 if proged else 0) 218 | (4 if erased else 0)) 219 220 @property 221 def wear(self): 222 return self >> 3 223 224 @property 225 def readed(self): 226 return (self & 1) != 0 227 228 @property 229 def proged(self): 230 return (self & 2) != 0 231 232 @property 233 def erased(self): 234 return (self & 4) != 0 235 236 def read(self): 237 return Block(int(self) | 1) 238 239 def prog(self): 240 return Block(int(self) | 2) 241 242 def erase(self): 243 return Block((int(self) | 4) + 8) 244 245 def clear(self): 246 return Block(int(self) & ~7) 247 248 def __or__(self, other): 249 return Block( 250 (int(self) | int(other)) & 7, 251 wear=max(self.wear, other.wear)) 252 253 def worn(self, max_wear, *, 254 block_cycles=None, 255 wear_chars=None, 256 **_): 257 if wear_chars is None: 258 wear_chars = WEAR_CHARS 259 260 if block_cycles: 261 return self.wear / block_cycles 262 else: 263 return self.wear / max(max_wear, len(wear_chars)) 264 265 def draw(self, max_wear, char=None, *, 266 read=True, 267 prog=True, 268 erase=True, 269 wear=False, 270 block_cycles=None, 271 color=True, 272 subscripts=False, 273 dots=False, 274 braille=False, 275 chars=None, 276 wear_chars=None, 277 colors=None, 278 wear_colors=None, 279 **_): 280 # fallback to default chars/colors 281 if chars is None: 282 chars = CHARS 283 if len(chars) < len(CHARS): 284 chars = chars + CHARS[len(chars):] 285 286 if colors is None: 287 if braille or dots: 288 colors = COLORS_DOTS 289 else: 290 colors = COLORS 291 if len(colors) < len(COLORS): 292 colors = colors + COLORS[len(colors):] 293 294 if wear_chars is None: 295 if subscripts: 296 wear_chars = WEAR_CHARS_SUBSCRIPTS 297 else: 298 wear_chars = WEAR_CHARS 299 300 if wear_colors is None: 301 wear_colors = WEAR_COLORS 302 303 # compute char/color 304 c = chars[3] 305 f = [colors[3]] 306 307 if wear: 308 w = min( 309 self.worn( 310 max_wear, 311 block_cycles=block_cycles, 312 wear_chars=wear_chars), 313 1) 314 315 c = wear_chars[int(w * (len(wear_chars)-1))] 316 f.append(wear_colors[int(w * (len(wear_colors)-1))]) 317 318 if erase and self.erased: 319 c = chars[2] 320 f.append(colors[2]) 321 elif prog and self.proged: 322 c = chars[1] 323 f.append(colors[1]) 324 elif read and self.readed: 325 c = chars[0] 326 f.append(colors[0]) 327 328 # override char? 329 if char: 330 c = char 331 332 # apply colors 333 if f and color: 334 c = '%s%s\x1b[m' % ( 335 ''.join('\x1b[%sm' % f_ for f_ in f), 336 c) 337 338 return c 339 340 341class Bd: 342 def __init__(self, *, 343 size=1, 344 count=1, 345 width=None, 346 height=1, 347 blocks=None): 348 if width is None: 349 width = count 350 351 if blocks is None: 352 self.blocks = [Block() for _ in range(width*height)] 353 else: 354 self.blocks = blocks 355 self.size = size 356 self.count = count 357 self.width = width 358 self.height = height 359 360 def _op(self, f, block=None, off=None, size=None): 361 if block is None: 362 range_ = range(len(self.blocks)) 363 else: 364 if off is None: 365 off, size = 0, self.size 366 elif size is None: 367 off, size = 0, off 368 369 # update our geometry? this will do nothing if we haven't changed 370 self.resize( 371 size=max(self.size, off+size), 372 count=max(self.count, block+1)) 373 374 # map to our block space 375 start = (block*self.size + off) / (self.size*self.count) 376 stop = (block*self.size + off+size) / (self.size*self.count) 377 378 range_ = range( 379 m.floor(start*len(self.blocks)), 380 m.ceil(stop*len(self.blocks))) 381 382 # apply the op 383 for i in range_: 384 self.blocks[i] = f(self.blocks[i]) 385 386 def read(self, block=None, off=None, size=None): 387 self._op(Block.read, block, off, size) 388 389 def prog(self, block=None, off=None, size=None): 390 self._op(Block.prog, block, off, size) 391 392 def erase(self, block=None, off=None, size=None): 393 self._op(Block.erase, block, off, size) 394 395 def clear(self, block=None, off=None, size=None): 396 self._op(Block.clear, block, off, size) 397 398 def copy(self): 399 return Bd( 400 blocks=self.blocks.copy(), 401 size=self.size, 402 count=self.count, 403 width=self.width, 404 height=self.height) 405 406 def resize(self, *, 407 size=None, 408 count=None, 409 width=None, 410 height=None): 411 size = size if size is not None else self.size 412 count = count if count is not None else self.count 413 width = width if width is not None else self.width 414 height = height if height is not None else self.height 415 416 if (size == self.size 417 and count == self.count 418 and width == self.width 419 and height == self.height): 420 return 421 422 # transform our blocks 423 blocks = [] 424 for x in range(width*height): 425 # map from new bd space 426 start = m.floor(x * (size*count)/(width*height)) 427 stop = m.ceil((x+1) * (size*count)/(width*height)) 428 start_block = start // size 429 start_off = start % size 430 stop_block = stop // size 431 stop_off = stop % size 432 # map to old bd space 433 start = start_block*self.size + start_off 434 stop = stop_block*self.size + stop_off 435 start = m.floor(start * len(self.blocks)/(self.size*self.count)) 436 stop = m.ceil(stop * len(self.blocks)/(self.size*self.count)) 437 438 # aggregate state 439 blocks.append(ft.reduce( 440 Block.__or__, 441 self.blocks[start:stop], 442 Block())) 443 444 self.size = size 445 self.count = count 446 self.width = width 447 self.height = height 448 self.blocks = blocks 449 450 def draw(self, row, *, 451 read=False, 452 prog=False, 453 erase=False, 454 wear=False, 455 hilbert=False, 456 lebesgue=False, 457 dots=False, 458 braille=False, 459 **args): 460 # find max wear? 461 max_wear = None 462 if wear: 463 max_wear = max(b.wear for b in self.blocks) 464 465 # fold via a curve? 466 if hilbert: 467 grid = [None]*(self.width*self.height) 468 for (x,y), b in zip( 469 hilbert_curve(self.width, self.height), 470 self.blocks): 471 grid[x + y*self.width] = b 472 elif lebesgue: 473 grid = [None]*(self.width*self.height) 474 for (x,y), b in zip( 475 lebesgue_curve(self.width, self.height), 476 self.blocks): 477 grid[x + y*self.width] = b 478 else: 479 grid = self.blocks 480 481 # need to wait for more trace output before rendering 482 # 483 # this is sort of a hack that knows the output is going to a terminal 484 if (braille and self.height < 4) or (dots and self.height < 2): 485 needed_height = 4 if braille else 2 486 487 self.history = getattr(self, 'history', []) 488 self.history.append(grid) 489 490 if len(self.history)*self.height < needed_height: 491 # skip for now 492 return None 493 494 grid = list(it.chain.from_iterable( 495 # did we resize? 496 it.islice(it.chain(h, it.repeat(Block())), 497 self.width*self.height) 498 for h in self.history)) 499 self.history = [] 500 501 line = [] 502 if braille: 503 # encode into a byte 504 for x in range(0, self.width, 2): 505 byte_b = 0 506 best_b = Block() 507 for i in range(2*4): 508 b = grid[x+(2-1-(i%2)) + ((row*4)+(4-1-(i//2)))*self.width] 509 best_b |= b 510 if ((read and b.readed) 511 or (prog and b.proged) 512 or (erase and b.erased) 513 or (not read and not prog and not erase 514 and wear and b.worn(max_wear, **args) >= 0.7)): 515 byte_b |= 1 << i 516 517 line.append(best_b.draw( 518 max_wear, 519 CHARS_BRAILLE[byte_b], 520 braille=True, 521 read=read, 522 prog=prog, 523 erase=erase, 524 wear=wear, 525 **args)) 526 elif dots: 527 # encode into a byte 528 for x in range(self.width): 529 byte_b = 0 530 best_b = Block() 531 for i in range(2): 532 b = grid[x + ((row*2)+(2-1-i))*self.width] 533 best_b |= b 534 if ((read and b.readed) 535 or (prog and b.proged) 536 or (erase and b.erased) 537 or (not read and not prog and not erase 538 and wear and b.worn(max_wear, **args) >= 0.7)): 539 byte_b |= 1 << i 540 541 line.append(best_b.draw( 542 max_wear, 543 CHARS_DOTS[byte_b], 544 dots=True, 545 read=read, 546 prog=prog, 547 erase=erase, 548 wear=wear, 549 **args)) 550 else: 551 for x in range(self.width): 552 line.append(grid[x + row*self.width].draw( 553 max_wear, 554 read=read, 555 prog=prog, 556 erase=erase, 557 wear=wear, 558 **args)) 559 560 return ''.join(line) 561 562 563 564def main(path='-', *, 565 read=False, 566 prog=False, 567 erase=False, 568 wear=False, 569 block=(None,None), 570 off=(None,None), 571 block_size=None, 572 block_count=None, 573 block_cycles=None, 574 reset=False, 575 color='auto', 576 dots=False, 577 braille=False, 578 width=None, 579 height=None, 580 lines=None, 581 cat=False, 582 hilbert=False, 583 lebesgue=False, 584 coalesce=None, 585 sleep=None, 586 keep_open=False, 587 **args): 588 # figure out what color should be 589 if color == 'auto': 590 color = sys.stdout.isatty() 591 elif color == 'always': 592 color = True 593 else: 594 color = False 595 596 # exclusive wear or read/prog/erase by default 597 if not read and not prog and not erase and not wear: 598 read = True 599 prog = True 600 erase = True 601 602 # assume a reasonable lines/height if not specified 603 # 604 # note that we let height = None if neither hilbert or lebesgue 605 # are specified, this is a bit special as the default may be less 606 # than one character in height. 607 if height is None and (hilbert or lebesgue): 608 if lines is not None: 609 height = lines 610 else: 611 height = 5 612 613 if lines is None: 614 if height is not None: 615 lines = height 616 else: 617 lines = 5 618 619 # allow ranges for blocks/offs 620 block_start = block[0] 621 block_stop = block[1] if len(block) > 1 else block[0]+1 622 off_start = off[0] 623 off_stop = off[1] if len(off) > 1 else off[0]+1 624 625 if block_start is None: 626 block_start = 0 627 if block_stop is None and block_count is not None: 628 block_stop = block_count 629 if off_start is None: 630 off_start = 0 631 if off_stop is None and block_size is not None: 632 off_stop = block_size 633 634 # create a block device representation 635 bd = Bd() 636 637 def resize(*, size=None, count=None): 638 nonlocal bd 639 640 # size may be overriden by cli args 641 if block_size is not None: 642 size = block_size 643 elif off_stop is not None: 644 size = off_stop-off_start 645 646 if block_count is not None: 647 count = block_count 648 elif block_stop is not None: 649 count = block_stop-block_start 650 651 # figure out best width/height 652 if width is None: 653 width_ = min(80, shutil.get_terminal_size((80, 5))[0]) 654 elif width: 655 width_ = width 656 else: 657 width_ = shutil.get_terminal_size((80, 5))[0] 658 659 if height is None: 660 height_ = 0 661 elif height: 662 height_ = height 663 else: 664 height_ = shutil.get_terminal_size((80, 5))[1] 665 666 bd.resize( 667 size=size, 668 count=count, 669 # scale if we're printing with dots or braille 670 width=2*width_ if braille else width_, 671 height=max(1, 672 4*height_ if braille 673 else 2*height_ if dots 674 else height_)) 675 resize() 676 677 # parse a line of trace output 678 pattern = re.compile( 679 '^(?P<file>[^:]*):(?P<line>[0-9]+):trace:.*?bd_(?:' 680 '(?P<create>create\w*)\(' 681 '(?:' 682 'block_size=(?P<block_size>\w+)' 683 '|' 'block_count=(?P<block_count>\w+)' 684 '|' '.*?' ')*' '\)' 685 '|' '(?P<read>read)\(' 686 '\s*(?P<read_ctx>\w+)' '\s*,' 687 '\s*(?P<read_block>\w+)' '\s*,' 688 '\s*(?P<read_off>\w+)' '\s*,' 689 '\s*(?P<read_buffer>\w+)' '\s*,' 690 '\s*(?P<read_size>\w+)' '\s*\)' 691 '|' '(?P<prog>prog)\(' 692 '\s*(?P<prog_ctx>\w+)' '\s*,' 693 '\s*(?P<prog_block>\w+)' '\s*,' 694 '\s*(?P<prog_off>\w+)' '\s*,' 695 '\s*(?P<prog_buffer>\w+)' '\s*,' 696 '\s*(?P<prog_size>\w+)' '\s*\)' 697 '|' '(?P<erase>erase)\(' 698 '\s*(?P<erase_ctx>\w+)' '\s*,' 699 '\s*(?P<erase_block>\w+)' 700 '\s*\(\s*(?P<erase_size>\w+)\s*\)' '\s*\)' 701 '|' '(?P<sync>sync)\(' 702 '\s*(?P<sync_ctx>\w+)' '\s*\)' ')\s*$') 703 def parse(line): 704 nonlocal bd 705 706 # string searching is much faster than the regex here, and this 707 # actually has a big impact given how much trace output comes 708 # through here 709 if 'trace' not in line or 'bd' not in line: 710 return False 711 m = pattern.match(line) 712 if not m: 713 return False 714 715 if m.group('create'): 716 # update our block size/count 717 size = int(m.group('block_size'), 0) 718 count = int(m.group('block_count'), 0) 719 720 resize(size=size, count=count) 721 if reset: 722 bd = Bd( 723 size=bd.size, 724 count=bd.count, 725 width=bd.width, 726 height=bd.height) 727 return True 728 729 elif m.group('read') and read: 730 block = int(m.group('read_block'), 0) 731 off = int(m.group('read_off'), 0) 732 size = int(m.group('read_size'), 0) 733 734 if block_stop is not None and block >= block_stop: 735 return False 736 block -= block_start 737 if off_stop is not None: 738 if off >= off_stop: 739 return False 740 size = min(size, off_stop-off) 741 off -= off_start 742 743 bd.read(block, off, size) 744 return True 745 746 elif m.group('prog') and prog: 747 block = int(m.group('prog_block'), 0) 748 off = int(m.group('prog_off'), 0) 749 size = int(m.group('prog_size'), 0) 750 751 if block_stop is not None and block >= block_stop: 752 return False 753 block -= block_start 754 if off_stop is not None: 755 if off >= off_stop: 756 return False 757 size = min(size, off_stop-off) 758 off -= off_start 759 760 bd.prog(block, off, size) 761 return True 762 763 elif m.group('erase') and (erase or wear): 764 block = int(m.group('erase_block'), 0) 765 size = int(m.group('erase_size'), 0) 766 767 if block_stop is not None and block >= block_stop: 768 return False 769 block -= block_start 770 if off_stop is not None: 771 size = min(size, off_stop) 772 off = -off_start 773 774 bd.erase(block, off, size) 775 return True 776 777 else: 778 return False 779 780 # print trace output 781 def draw(f): 782 def writeln(s=''): 783 f.write(s) 784 f.write('\n') 785 f.writeln = writeln 786 787 # don't forget we've scaled this for braille/dots! 788 for row in range( 789 m.ceil(bd.height/4) if braille 790 else m.ceil(bd.height/2) if dots 791 else bd.height): 792 line = bd.draw(row, 793 read=read, 794 prog=prog, 795 erase=erase, 796 wear=wear, 797 block_cycles=block_cycles, 798 color=color, 799 dots=dots, 800 braille=braille, 801 hilbert=hilbert, 802 lebesgue=lebesgue, 803 **args) 804 if line: 805 f.writeln(line) 806 807 bd.clear() 808 resize() 809 810 811 # read/parse/coalesce operations 812 if cat: 813 ring = sys.stdout 814 else: 815 ring = LinesIO(lines) 816 817 # if sleep print in background thread to avoid getting stuck in a read call 818 event = th.Event() 819 lock = th.Lock() 820 if sleep: 821 done = False 822 def background(): 823 while not done: 824 event.wait() 825 event.clear() 826 with lock: 827 draw(ring) 828 if not cat: 829 ring.draw() 830 time.sleep(sleep or 0.01) 831 th.Thread(target=background, daemon=True).start() 832 833 try: 834 while True: 835 with openio(path) as f: 836 changed = 0 837 for line in f: 838 with lock: 839 changed += parse(line) 840 841 # need to redraw? 842 if changed and (not coalesce or changed >= coalesce): 843 if sleep: 844 event.set() 845 else: 846 draw(ring) 847 if not cat: 848 ring.draw() 849 changed = 0 850 851 if not keep_open: 852 break 853 # don't just flood open calls 854 time.sleep(sleep or 0.1) 855 except FileNotFoundError as e: 856 print("error: file not found %r" % path) 857 sys.exit(-1) 858 except KeyboardInterrupt: 859 pass 860 861 if sleep: 862 done = True 863 lock.acquire() # avoids https://bugs.python.org/issue42717 864 if not cat: 865 sys.stdout.write('\n') 866 867 868if __name__ == "__main__": 869 import sys 870 import argparse 871 parser = argparse.ArgumentParser( 872 description="Display operations on block devices based on " 873 "trace output.", 874 allow_abbrev=False) 875 parser.add_argument( 876 'path', 877 nargs='?', 878 help="Path to read from.") 879 parser.add_argument( 880 '-r', '--read', 881 action='store_true', 882 help="Render reads.") 883 parser.add_argument( 884 '-p', '--prog', 885 action='store_true', 886 help="Render progs.") 887 parser.add_argument( 888 '-e', '--erase', 889 action='store_true', 890 help="Render erases.") 891 parser.add_argument( 892 '-w', '--wear', 893 action='store_true', 894 help="Render wear.") 895 parser.add_argument( 896 '-b', '--block', 897 type=lambda x: tuple( 898 int(x, 0) if x.strip() else None 899 for x in x.split(',')), 900 help="Show a specific block or range of blocks.") 901 parser.add_argument( 902 '-i', '--off', 903 type=lambda x: tuple( 904 int(x, 0) if x.strip() else None 905 for x in x.split(',')), 906 help="Show a specific offset or range of offsets.") 907 parser.add_argument( 908 '-B', '--block-size', 909 type=lambda x: int(x, 0), 910 help="Assume a specific block size.") 911 parser.add_argument( 912 '--block-count', 913 type=lambda x: int(x, 0), 914 help="Assume a specific block count.") 915 parser.add_argument( 916 '-C', '--block-cycles', 917 type=lambda x: int(x, 0), 918 help="Assumed maximum number of erase cycles when measuring wear.") 919 parser.add_argument( 920 '-R', '--reset', 921 action='store_true', 922 help="Reset wear on block device initialization.") 923 parser.add_argument( 924 '--color', 925 choices=['never', 'always', 'auto'], 926 default='auto', 927 help="When to use terminal colors. Defaults to 'auto'.") 928 parser.add_argument( 929 '--subscripts', 930 action='store_true', 931 help="Use unicode subscripts for showing wear.") 932 parser.add_argument( 933 '-:', '--dots', 934 action='store_true', 935 help="Use 1x2 ascii dot characters.") 936 parser.add_argument( 937 '-⣿', '--braille', 938 action='store_true', 939 help="Use 2x4 unicode braille characters. Note that braille characters " 940 "sometimes suffer from inconsistent widths.") 941 parser.add_argument( 942 '--chars', 943 help="Characters to use for read, prog, erase, noop operations.") 944 parser.add_argument( 945 '--wear-chars', 946 help="Characters to use for showing wear.") 947 parser.add_argument( 948 '--colors', 949 type=lambda x: [x.strip() for x in x.split(',')], 950 help="Colors to use for read, prog, erase, noop operations.") 951 parser.add_argument( 952 '--wear-colors', 953 type=lambda x: [x.strip() for x in x.split(',')], 954 help="Colors to use for showing wear.") 955 parser.add_argument( 956 '-W', '--width', 957 nargs='?', 958 type=lambda x: int(x, 0), 959 const=0, 960 help="Width in columns. 0 uses the terminal width. Defaults to " 961 "min(terminal, 80).") 962 parser.add_argument( 963 '-H', '--height', 964 nargs='?', 965 type=lambda x: int(x, 0), 966 const=0, 967 help="Height in rows. 0 uses the terminal height. Defaults to 1.") 968 parser.add_argument( 969 '-n', '--lines', 970 nargs='?', 971 type=lambda x: int(x, 0), 972 const=0, 973 help="Show this many lines of history. 0 uses the terminal height. " 974 "Defaults to 5.") 975 parser.add_argument( 976 '-z', '--cat', 977 action='store_true', 978 help="Pipe directly to stdout.") 979 parser.add_argument( 980 '-U', '--hilbert', 981 action='store_true', 982 help="Render as a space-filling Hilbert curve.") 983 parser.add_argument( 984 '-Z', '--lebesgue', 985 action='store_true', 986 help="Render as a space-filling Z-curve.") 987 parser.add_argument( 988 '-c', '--coalesce', 989 type=lambda x: int(x, 0), 990 help="Number of operations to coalesce together.") 991 parser.add_argument( 992 '-s', '--sleep', 993 type=float, 994 help="Time in seconds to sleep between reads, coalescing operations.") 995 parser.add_argument( 996 '-k', '--keep-open', 997 action='store_true', 998 help="Reopen the pipe on EOF, useful when multiple " 999 "processes are writing.") 1000 sys.exit(main(**{k: v 1001 for k, v in vars(parser.parse_intermixed_args()).items() 1002 if v is not None})) 1003