1# -*- coding: utf-8 -*- 2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Operation, including output and progress display 7 8This module implements the concept of an operation, which has regular progress 9updates, verbose text display and perhaps some errors. 10""" 11 12from __future__ import division 13from __future__ import print_function 14 15import collections 16import contextlib 17import fcntl 18import multiprocessing 19import os 20import pty 21import re 22import struct 23import sys 24import termios 25 26from six.moves import queue as Queue 27 28from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging 29from autotest_lib.utils.frozen_chromite.lib import osutils 30from autotest_lib.utils.frozen_chromite.lib import parallel 31from autotest_lib.utils.frozen_chromite.lib.terminal import Color 32from autotest_lib.utils.frozen_chromite.utils import outcap 33 34 35# Define filenames for captured stdout and stderr. 36STDOUT_FILE = 'stdout' 37STDERR_FILE = 'stderr' 38 39_TerminalSize = collections.namedtuple('_TerminalSize', ('lines', 'columns')) 40 41 42class _BackgroundTaskComplete(object): 43 """Sentinal object to indicate that the background task is complete.""" 44 45 46class ProgressBarOperation(object): 47 """Wrapper around long running functions to show progress. 48 49 This class is intended to capture the output of a long running fuction, parse 50 the output, and display a progress bar. 51 52 To display a progress bar for a function foo with argument foo_args, this is 53 the usage case: 54 1) Create a class that inherits from ProgressBarOperation (e.g. 55 FooTypeOperation. In this class, override the ParseOutput method to parse 56 the output of foo. 57 2) op = operation.FooTypeOperation() 58 op.Run(foo, foo_args) 59 """ 60 61 # Subtract 10 characters from the width of the terminal because these are used 62 # to display the percentage as well as other spaces. 63 _PROGRESS_BAR_BORDER_SIZE = 10 64 65 # By default, update the progress bar every 100 ms. 66 _PROGRESS_BAR_UPDATE_INTERVAL = 0.1 67 68 def __init__(self): 69 self._queue = multiprocessing.Queue() 70 self._stderr = None 71 self._stdout = None 72 self._stdout_path = None 73 self._stderr_path = None 74 self._progress_bar_displayed = False 75 self._isatty = os.isatty(sys.stdout.fileno()) 76 77 def _GetTerminalSize(self, fd=pty.STDOUT_FILENO): 78 """Return a terminal size object for |fd|. 79 80 Note: Replace with os.terminal_size() in python3.3. 81 """ 82 winsize = struct.pack('HHHH', 0, 0, 0, 0) 83 data = fcntl.ioctl(fd, termios.TIOCGWINSZ, winsize) 84 winsize = struct.unpack('HHHH', data) 85 return _TerminalSize(int(winsize[0]), int(winsize[1])) 86 87 def ProgressBar(self, progress): 88 """This method creates and displays a progress bar. 89 90 If not in a terminal, we do not display a progress bar. 91 92 Args: 93 progress: a float between 0 and 1 that represents the fraction of the 94 current progress. 95 """ 96 if not self._isatty: 97 return 98 self._progress_bar_displayed = True 99 progress = max(0.0, min(1.0, progress)) 100 width = max(1, self._GetTerminalSize().columns - 101 self._PROGRESS_BAR_BORDER_SIZE) 102 block = int(width * progress) 103 shaded = '#' * block 104 unshaded = '-' * (width - block) 105 text = '\r [%s%s] %d%%' % (shaded, unshaded, progress * 100) 106 sys.stdout.write(text) 107 sys.stdout.flush() 108 109 def OpenStdoutStderr(self): 110 """Open the stdout and stderr streams.""" 111 if self._stdout is None and self._stderr is None: 112 self._stdout = open(self._stdout_path, 'r') 113 self._stderr = open(self._stderr_path, 'r') 114 115 def Cleanup(self): 116 """Method to cleanup progress bar. 117 118 If progress bar has been printed, then we make sure it displays 100% before 119 exiting. 120 """ 121 if self._progress_bar_displayed: 122 self.ProgressBar(1) 123 sys.stdout.write('\n') 124 sys.stdout.flush() 125 126 def ParseOutput(self, output=None): 127 """Method to parse output and update progress bar. 128 129 This method should be overridden to read and parse the lines in _stdout and 130 _stderr. 131 132 One example use of this method could be to detect 'foo' in stdout and 133 increment the progress bar every time foo is seen. 134 135 def ParseOutput(self): 136 stdout = self._stdout.read() 137 if 'foo' in stdout: 138 # Increment progress bar. 139 140 Args: 141 output: Pass in output to parse instead of reading from self._stdout and 142 self._stderr. 143 """ 144 raise NotImplementedError('Subclass must override this method.') 145 146 # TODO(ralphnathan): Deprecate this function and use parallel._BackgroundTask 147 # instead (brbug.com/863) 148 def WaitUntilComplete(self, update_period): 149 """Return True if running background task has completed.""" 150 try: 151 x = self._queue.get(timeout=update_period) 152 if isinstance(x, _BackgroundTaskComplete): 153 return True 154 except Queue.Empty: 155 return False 156 157 def CaptureOutputInBackground(self, func, *args, **kwargs): 158 """Launch func in background and capture its output. 159 160 Args: 161 func: Function to execute in the background and whose output is to be 162 captured. 163 log_level: Logging level to run the func at. By default, it runs at log 164 level info. 165 """ 166 log_level = kwargs.pop('log_level', logging.INFO) 167 restore_log_level = logging.getLogger().getEffectiveLevel() 168 logging.getLogger().setLevel(log_level) 169 try: 170 with outcap.OutputCapturer( 171 stdout_path=self._stdout_path, stderr_path=self._stderr_path, 172 quiet_fail=False): 173 func(*args, **kwargs) 174 finally: 175 self._queue.put(_BackgroundTaskComplete()) 176 logging.getLogger().setLevel(restore_log_level) 177 178 # TODO (ralphnathan): Store PID of spawned process. 179 def Run(self, func, *args, **kwargs): 180 """Run func, parse its output, and update the progress bar. 181 182 Args: 183 func: Function to execute in the background and whose output is to be 184 captured. 185 update_period: Optional argument to specify the period that output should 186 be read. 187 log_level: Logging level to run the func at. By default, it runs at log 188 level info. 189 """ 190 update_period = kwargs.pop('update_period', 191 self._PROGRESS_BAR_UPDATE_INTERVAL) 192 193 # If we are not running in a terminal device, do not display the progress 194 # bar. 195 if not self._isatty: 196 log_level = kwargs.pop('log_level', logging.INFO) 197 restore_log_level = logging.getLogger().getEffectiveLevel() 198 logging.getLogger().setLevel(log_level) 199 try: 200 func(*args, **kwargs) 201 finally: 202 logging.getLogger().setLevel(restore_log_level) 203 return 204 205 with osutils.TempDir() as tempdir: 206 self._stdout_path = os.path.join(tempdir, STDOUT_FILE) 207 self._stderr_path = os.path.join(tempdir, STDERR_FILE) 208 osutils.Touch(self._stdout_path) 209 osutils.Touch(self._stderr_path) 210 try: 211 with parallel.BackgroundTaskRunner( 212 self.CaptureOutputInBackground, func, *args, **kwargs) as queue: 213 queue.put([]) 214 self.OpenStdoutStderr() 215 while True: 216 self.ParseOutput() 217 if self.WaitUntilComplete(update_period): 218 break 219 # Before we exit, parse the output again to update progress bar. 220 self.ParseOutput() 221 # Final sanity check to update the progress bar to 100% if it was used 222 # by ParseOutput 223 self.Cleanup() 224 except: 225 # Add a blank line before the logging message so the message isn't 226 # touching the progress bar. 227 sys.stdout.write('\n') 228 logging.error('Oops. Something went wrong.') 229 # Raise the exception so it can be caught again. 230 raise 231 232 233class ParallelEmergeOperation(ProgressBarOperation): 234 """ProgressBarOperation specific for scripts/parallel_emerge.py.""" 235 236 def __init__(self): 237 super(ParallelEmergeOperation, self).__init__() 238 self._total = None 239 self._completed = 0 240 self._printed_no_packages = False 241 self._events = ['Fetched ', 'Completed '] 242 self._msg = None 243 244 def _GetTotal(self, output): 245 """Get total packages by looking for Total: digits packages.""" 246 match = re.search(r'Total: (\d+) packages', output) 247 return int(match.group(1)) if match else None 248 249 def SetProgressBarMessage(self, msg): 250 """Message to be shown before the progress bar is displayed with 0%. 251 252 The message is not displayed if the progress bar is not going to be 253 displayed. 254 """ 255 self._msg = msg 256 257 def ParseOutput(self, output=None): 258 """Parse the output of emerge to determine how to update progress bar. 259 260 1) Figure out how many packages exist. If the total number of packages to be 261 built is zero, then we do not display the progress bar. 262 2) Whenever a package is downloaded or built, 'Fetched' and 'Completed' are 263 printed respectively. By counting counting 'Fetched's and 'Completed's, we 264 can determine how much to update the progress bar by. 265 266 Args: 267 output: Pass in output to parse instead of reading from self._stdout and 268 self._stderr. 269 270 Returns: 271 A fraction between 0 and 1 indicating the level of the progress bar. If 272 the progress bar isn't displayed, then the return value is -1. 273 """ 274 if output is None: 275 stdout = self._stdout.read() 276 stderr = self._stderr.read() 277 output = stdout + stderr 278 279 if self._total is None: 280 temp = self._GetTotal(output) 281 if temp is not None: 282 self._total = temp * len(self._events) 283 if self._msg is not None: 284 logging.notice(self._msg) 285 286 for event in self._events: 287 self._completed += output.count(event) 288 289 if not self._printed_no_packages and self._total == 0: 290 logging.notice('No packages to build.') 291 self._printed_no_packages = True 292 293 if self._total: 294 progress = self._completed / self._total 295 self.ProgressBar(progress) 296 return progress 297 else: 298 return -1 299 300 301# TODO(sjg): When !isatty(), keep stdout and stderr separate so they can be 302# redirected separately 303# TODO(sjg): Add proper docs to this fileno 304# TODO(sjg): Handle stdin wait in quite mode, rather than silently stalling 305 306class Operation(object): 307 """Class which controls stdio and progress of an operation in progress. 308 309 This class is created to handle stdio for a running subprocess. It filters 310 it looking for errors and progress information. Optionally it can output the 311 stderr and stdout to the terminal, but it is normally supressed. 312 313 Progress information is garnered from the subprocess output based on 314 knowledge of the legacy scripts, but at some point will move over to using 315 real progress information reported through new python methods which will 316 replace the scripts. 317 318 Each operation has a name, and this class handles displaying this name 319 as it reports progress. 320 321 Operation Objects 322 ================= 323 324 verbose: True / False 325 In verbose mode all output from subprocesses is displayed, otherwise 326 this output is normally supressed, unless we think it indicates an error. 327 328 progress: True / False 329 The output from subprocesses can be analysed in a very basic manner to 330 try to present progress information to the user. 331 332 explicit_verbose: True / False 333 False if we are not just using default verbosity. In that case we allow 334 verbosity to be enabled on request, since the user has not explicitly 335 disabled it. This is used by commands that the user issues with the 336 expectation that output would ordinarily be visible. 337 """ 338 339 def __init__(self, name, color=None): 340 """Create a new operation. 341 342 Args: 343 name: Operation name in a form to be displayed for the user. 344 color: Determines policy for sending color to stdout; see terminal.Color 345 for details on interpretation on the value. 346 """ 347 self._name = name # Operation name. 348 self.verbose = False # True to echo subprocess output. 349 self.progress = True # True to report progress of the operation 350 self._column = 0 # Current output column (always 0 unless verbose). 351 self._update_len = 0 # Length of last progress update message. 352 self._line = '' # text of current line, so far 353 self.explicit_verbose = False 354 355 self._color = Color(enabled=color) 356 357 # -1 = no newline pending 358 # n = newline pending, and line length of last line was n 359 self._pending_nl = -1 360 361 # the type of the last stream to emit data on the current lines 362 # can be sys.stdout, sys.stderr (both from the subprocess), or None 363 # for our own mesages 364 self._cur_stream = None 365 366 self._error_count = 0 # number of error lines we have reported 367 368 def __del__(self): 369 """Object is about to be destroyed, so finish out output cleanly.""" 370 self.FinishOutput() 371 372 def FinishOutput(self): 373 """Finish off any pending output. 374 375 This finishes any output line currently in progress and resets the color 376 back to normal. 377 """ 378 self._FinishLine(self.verbose, final=True) 379 if self._column and self.verbose: 380 print(self._color.Stop()) 381 self._column = 0 382 383 def WereErrorsDetected(self): 384 """Returns whether any errors have been detected. 385 386 Returns: 387 True if any errors have been detected in subprocess output so far. 388 False otherwise 389 """ 390 return self._error_count > 0 391 392 def SetName(self, name): 393 """Set the name of the operation as displayed to the user. 394 395 Args: 396 name: Operation name. 397 """ 398 self._name = name 399 400 def _FilterOutputForErrors(self, line, print_error): 401 """Filter a line of output to look for and display errors. 402 403 This uses a few regular expression searches to spot common error reports 404 from subprocesses. A count of these is kept so we know how many occurred. 405 Optionally they are displayed in red on the terminal. 406 407 Args: 408 line: the output line to filter, as a string. 409 print_error: True to print the error, False to just record it. 410 """ 411 bad_things = ['Cannot GET', 'ERROR', '!!!', 'FAILED'] 412 for bad_thing in bad_things: 413 if re.search(bad_thing, line, flags=re.IGNORECASE): 414 self._error_count += 1 415 if print_error: 416 print(self._color.Color(self._color.RED, line)) 417 break 418 419 def _FilterOutputForProgress(self, line): 420 """Filter a line of output to look for and dispay progress information. 421 422 This uses a simple regular expression search to spot progress information 423 coming from subprocesses. This is sent to the _Progress() method. 424 425 Args: 426 line: the output line to filter, as a string. 427 """ 428 match = re.match(r'Pending (\d+).*Total (\d+)', line) 429 if match: 430 pending = int(match.group(1)) 431 total = int(match.group(2)) 432 self._Progress(total - pending, total) 433 434 def _Progress(self, upto, total): 435 """Record and optionally display progress information. 436 437 Args: 438 upto: which step we are up to in the operation (integer, from 0). 439 total: total number of steps in operation, 440 """ 441 if total > 0: 442 update_str = '%s...%d%% (%d of %d)' % (self._name, 443 upto * 100 // total, upto, total) 444 if self.progress: 445 # Finish the current line, print progress, and remember its length. 446 self._FinishLine(self.verbose) 447 448 # Sometimes the progress string shrinks and in this case we need to 449 # blank out the characters at the end of the line that will not be 450 # overwritten by the new line 451 pad = max(self._update_len - len(update_str), 0) 452 sys.stdout.write(update_str + (' ' * pad) + '\r') 453 self._update_len = len(update_str) 454 455 def _FinishLine(self, display, final=False): 456 """Finish off the current line and prepare to start a new one. 457 458 If a new line is pending from the previous line, then this will be output, 459 along with a color reset if needed. 460 461 We also handle removing progress messages from the output. This is done 462 using a carriage return character, following by spaces. 463 464 Args: 465 display: True to display output, False to suppress it 466 final: True if this is the final output before we exit, in which case 467 we must clean up any remaining progress message by overwriting 468 it with spaces, then carriage return 469 """ 470 if display: 471 if self._pending_nl != -1: 472 # If out last output line was shorter than the progress info 473 # add spaces. 474 if self._pending_nl < self._update_len: 475 print(' ' * (self._update_len - self._pending_nl), end='') 476 477 # Output the newline, and reset our counter. 478 sys.stdout.write(self._color.Stop()) 479 print() 480 481 # If this is the last thing that this operation will print, we need to 482 # close things off. So if there is some text on the current line but not 483 # enough to overwrite all the progress information we have sent, add some 484 # more spaces. 485 if final and self._update_len: 486 print(' ' * self._update_len, '\r', end='') 487 488 self._pending_nl = -1 489 490 def _CheckStreamAndColor(self, stream, display): 491 """Check that we're writing to the same stream as last call. No? New line. 492 493 If starting a new line, set the color correctly: 494 stdout Magenta 495 stderr Red 496 other White / no colors 497 498 Args: 499 stream: The stream we're going to write to. 500 display: True to display it on terms, False to suppress it. 501 """ 502 if self._column > 0 and stream != self._cur_stream: 503 self._FinishLine(display) 504 if display: 505 print(self._color.Stop()) 506 507 self._column = 0 508 self._line = '' 509 510 # Use colors for child output. 511 if self._column == 0: 512 self._FinishLine(display) 513 if display: 514 color = None 515 if stream == sys.stdout: 516 color = self._color.MAGENTA 517 elif stream == sys.stderr: 518 color = self._color.RED 519 if color: 520 sys.stdout.write(self._color.Start(color)) 521 522 self._cur_stream = stream 523 524 def _Out(self, stream, text, display, newline=False, do_output_filter=True): 525 """Output some text received from a child, or generated internally. 526 527 This method is the guts of the Operation class since it understands how to 528 convert a series of output requests on different streams into something 529 coherent for the user. 530 531 If the stream has changed, then a new line is started even if we were 532 still halfway through the previous line. This prevents stdout and stderr 533 becoming mixed up quite so badly. 534 535 We use color to indicate lines which are stdout and stderr. If the output 536 received from the child has color codes in it already, we pass these 537 through, so our colors can be overridden. If output is redirected then we 538 do not add color by default. Note that nothing stops the child from adding 539 it, but since we present ourselves as a terminal to the child, one might 540 hope that the child will not generate color. 541 542 If display is False, then we will not actually send this text to the 543 terminal. This is uses when verbose is required to be False. 544 545 Args: 546 stream: stream on which the text was received: 547 sys.stdout - received on stdout 548 sys.stderr - received on stderr 549 None - generated by us / internally 550 text: text to output 551 display: True to display it on terms, False to suppress it 552 newline: True to start a new line after this text, False to put the next 553 lot of output immediately after this. 554 do_output_filter: True to look through output for errors and progress. 555 """ 556 self._CheckStreamAndColor(stream, display) 557 558 # Output what we have, and remember what column we are up to. 559 if display: 560 sys.stdout.write(text) 561 self._column += len(text) 562 # If a newline is required, remember to output it later. 563 if newline: 564 self._pending_nl = self._column 565 self._column = 0 566 567 self._line += text 568 569 # If we now have a whole line, check it for errors and progress. 570 if newline: 571 if do_output_filter: 572 self._FilterOutputForErrors(self._line, print_error=not display) 573 self._FilterOutputForProgress(self._line) 574 self._line = '' 575 576 def Output(self, stream, data): 577 r"""Handle the output of a block of text from the subprocess. 578 579 All subprocess output should be sent through this method. It is split into 580 lines which are processed separately using the _Out() method. 581 582 Args: 583 stream: Which file the output come in on: 584 sys.stdout: stdout 585 sys.stderr: stderr 586 None: Our own internal output 587 data: Output data as a big string, potentially containing many lines of 588 text. Each line should end with \r\n. There is no requirement to send 589 whole lines - this method happily handles fragments and tries to 590 present then to the user as early as possible 591 592 #TODO(sjg): Just use a list as the input parameter to avoid the split. 593 """ 594 # We cannot use splitlines() here as we need this exact behavior 595 lines = data.split('\r\n') 596 597 # Output each full line, with a \n after it. 598 for line in lines[:-1]: 599 self._Out(stream, line, display=self.verbose, newline=True) 600 601 # If we have a partial line at the end, output what we have. 602 # We will continue it later. 603 if lines[-1]: 604 self._Out(stream, lines[-1], display=self.verbose) 605 606 # Flush so that the terminal will receive partial line output (now!) 607 sys.stdout.flush() 608 609 def Outline(self, line): 610 r"""Output a line of text to the display. 611 612 This outputs text generated internally, such as a warning message or error 613 summary. It ensures that our message plays nicely with child output if 614 any. 615 616 Args: 617 line: text to output (without \n on the end) 618 """ 619 self._Out(None, line, display=True, newline=True) 620 self._FinishLine(display=True) 621 622 def Info(self, line): 623 r"""Output a line of information text to the display in verbose mode. 624 625 Args: 626 line: text to output (without \n on the end) 627 """ 628 self._Out(None, self._color.Color(self._color.BLUE, line), 629 display=self.verbose, newline=True, do_output_filter=False) 630 self._FinishLine(display=True) 631 632 def Notice(self, line): 633 r"""Output a line of notification text to the display. 634 635 Args: 636 line: text to output (without \n on the end) 637 """ 638 self._Out(None, self._color.Color(self._color.GREEN, line), 639 display=True, newline=True, do_output_filter=False) 640 self._FinishLine(display=True) 641 642 def Warning(self, line): 643 r"""Output a line of warning text to the display. 644 645 Args: 646 line: text to output (without \n on the end) 647 """ 648 self._Out(None, self._color.Color(self._color.YELLOW, line), 649 display=True, newline=True, do_output_filter=False) 650 self._FinishLine(display=True) 651 652 def Error(self, line): 653 r"""Output a line of error text to the display. 654 655 Args: 656 line: text to output (without \n on the end) 657 """ 658 self._Out(None, self._color.Color(self._color.RED, line), 659 display=True, newline=True, do_output_filter=False) 660 self._FinishLine(display=True) 661 662 def Die(self, line): 663 r"""Output a line of error text to the display and die. 664 665 Args: 666 line: text to output (without \n on the end) 667 """ 668 self.Error(line) 669 sys.exit(1) 670 671 @contextlib.contextmanager 672 def RequestVerbose(self, request): 673 """Perform something in verbose mode if the user hasn't disallowed it 674 675 This is intended to be used with something like: 676 677 with oper.RequestVerbose(True): 678 ... do some things that generate output 679 680 Args: 681 request: True to request verbose mode if available, False to do nothing. 682 """ 683 old_verbose = self.verbose 684 if request and not self.explicit_verbose: 685 self.verbose = True 686 try: 687 yield 688 finally: 689 self.verbose = old_verbose 690