690 lines
23 KiB
Python
690 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""Operation, including output and progress display
|
|
|
|
This module implements the concept of an operation, which has regular progress
|
|
updates, verbose text display and perhaps some errors.
|
|
"""
|
|
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
|
|
import collections
|
|
import contextlib
|
|
import fcntl
|
|
import multiprocessing
|
|
import os
|
|
import pty
|
|
import re
|
|
import struct
|
|
import sys
|
|
import termios
|
|
|
|
from six.moves import queue as Queue
|
|
|
|
from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging
|
|
from autotest_lib.utils.frozen_chromite.lib import osutils
|
|
from autotest_lib.utils.frozen_chromite.lib import parallel
|
|
from autotest_lib.utils.frozen_chromite.lib.terminal import Color
|
|
from autotest_lib.utils.frozen_chromite.utils import outcap
|
|
|
|
|
|
# Define filenames for captured stdout and stderr.
|
|
STDOUT_FILE = 'stdout'
|
|
STDERR_FILE = 'stderr'
|
|
|
|
_TerminalSize = collections.namedtuple('_TerminalSize', ('lines', 'columns'))
|
|
|
|
|
|
class _BackgroundTaskComplete(object):
|
|
"""Sentinal object to indicate that the background task is complete."""
|
|
|
|
|
|
class ProgressBarOperation(object):
|
|
"""Wrapper around long running functions to show progress.
|
|
|
|
This class is intended to capture the output of a long running fuction, parse
|
|
the output, and display a progress bar.
|
|
|
|
To display a progress bar for a function foo with argument foo_args, this is
|
|
the usage case:
|
|
1) Create a class that inherits from ProgressBarOperation (e.g.
|
|
FooTypeOperation. In this class, override the ParseOutput method to parse
|
|
the output of foo.
|
|
2) op = operation.FooTypeOperation()
|
|
op.Run(foo, foo_args)
|
|
"""
|
|
|
|
# Subtract 10 characters from the width of the terminal because these are used
|
|
# to display the percentage as well as other spaces.
|
|
_PROGRESS_BAR_BORDER_SIZE = 10
|
|
|
|
# By default, update the progress bar every 100 ms.
|
|
_PROGRESS_BAR_UPDATE_INTERVAL = 0.1
|
|
|
|
def __init__(self):
|
|
self._queue = multiprocessing.Queue()
|
|
self._stderr = None
|
|
self._stdout = None
|
|
self._stdout_path = None
|
|
self._stderr_path = None
|
|
self._progress_bar_displayed = False
|
|
self._isatty = os.isatty(sys.stdout.fileno())
|
|
|
|
def _GetTerminalSize(self, fd=pty.STDOUT_FILENO):
|
|
"""Return a terminal size object for |fd|.
|
|
|
|
Note: Replace with os.terminal_size() in python3.3.
|
|
"""
|
|
winsize = struct.pack('HHHH', 0, 0, 0, 0)
|
|
data = fcntl.ioctl(fd, termios.TIOCGWINSZ, winsize)
|
|
winsize = struct.unpack('HHHH', data)
|
|
return _TerminalSize(int(winsize[0]), int(winsize[1]))
|
|
|
|
def ProgressBar(self, progress):
|
|
"""This method creates and displays a progress bar.
|
|
|
|
If not in a terminal, we do not display a progress bar.
|
|
|
|
Args:
|
|
progress: a float between 0 and 1 that represents the fraction of the
|
|
current progress.
|
|
"""
|
|
if not self._isatty:
|
|
return
|
|
self._progress_bar_displayed = True
|
|
progress = max(0.0, min(1.0, progress))
|
|
width = max(1, self._GetTerminalSize().columns -
|
|
self._PROGRESS_BAR_BORDER_SIZE)
|
|
block = int(width * progress)
|
|
shaded = '#' * block
|
|
unshaded = '-' * (width - block)
|
|
text = '\r [%s%s] %d%%' % (shaded, unshaded, progress * 100)
|
|
sys.stdout.write(text)
|
|
sys.stdout.flush()
|
|
|
|
def OpenStdoutStderr(self):
|
|
"""Open the stdout and stderr streams."""
|
|
if self._stdout is None and self._stderr is None:
|
|
self._stdout = open(self._stdout_path, 'r')
|
|
self._stderr = open(self._stderr_path, 'r')
|
|
|
|
def Cleanup(self):
|
|
"""Method to cleanup progress bar.
|
|
|
|
If progress bar has been printed, then we make sure it displays 100% before
|
|
exiting.
|
|
"""
|
|
if self._progress_bar_displayed:
|
|
self.ProgressBar(1)
|
|
sys.stdout.write('\n')
|
|
sys.stdout.flush()
|
|
|
|
def ParseOutput(self, output=None):
|
|
"""Method to parse output and update progress bar.
|
|
|
|
This method should be overridden to read and parse the lines in _stdout and
|
|
_stderr.
|
|
|
|
One example use of this method could be to detect 'foo' in stdout and
|
|
increment the progress bar every time foo is seen.
|
|
|
|
def ParseOutput(self):
|
|
stdout = self._stdout.read()
|
|
if 'foo' in stdout:
|
|
# Increment progress bar.
|
|
|
|
Args:
|
|
output: Pass in output to parse instead of reading from self._stdout and
|
|
self._stderr.
|
|
"""
|
|
raise NotImplementedError('Subclass must override this method.')
|
|
|
|
# TODO(ralphnathan): Deprecate this function and use parallel._BackgroundTask
|
|
# instead (brbug.com/863)
|
|
def WaitUntilComplete(self, update_period):
|
|
"""Return True if running background task has completed."""
|
|
try:
|
|
x = self._queue.get(timeout=update_period)
|
|
if isinstance(x, _BackgroundTaskComplete):
|
|
return True
|
|
except Queue.Empty:
|
|
return False
|
|
|
|
def CaptureOutputInBackground(self, func, *args, **kwargs):
|
|
"""Launch func in background and capture its output.
|
|
|
|
Args:
|
|
func: Function to execute in the background and whose output is to be
|
|
captured.
|
|
log_level: Logging level to run the func at. By default, it runs at log
|
|
level info.
|
|
"""
|
|
log_level = kwargs.pop('log_level', logging.INFO)
|
|
restore_log_level = logging.getLogger().getEffectiveLevel()
|
|
logging.getLogger().setLevel(log_level)
|
|
try:
|
|
with outcap.OutputCapturer(
|
|
stdout_path=self._stdout_path, stderr_path=self._stderr_path,
|
|
quiet_fail=False):
|
|
func(*args, **kwargs)
|
|
finally:
|
|
self._queue.put(_BackgroundTaskComplete())
|
|
logging.getLogger().setLevel(restore_log_level)
|
|
|
|
# TODO (ralphnathan): Store PID of spawned process.
|
|
def Run(self, func, *args, **kwargs):
|
|
"""Run func, parse its output, and update the progress bar.
|
|
|
|
Args:
|
|
func: Function to execute in the background and whose output is to be
|
|
captured.
|
|
update_period: Optional argument to specify the period that output should
|
|
be read.
|
|
log_level: Logging level to run the func at. By default, it runs at log
|
|
level info.
|
|
"""
|
|
update_period = kwargs.pop('update_period',
|
|
self._PROGRESS_BAR_UPDATE_INTERVAL)
|
|
|
|
# If we are not running in a terminal device, do not display the progress
|
|
# bar.
|
|
if not self._isatty:
|
|
log_level = kwargs.pop('log_level', logging.INFO)
|
|
restore_log_level = logging.getLogger().getEffectiveLevel()
|
|
logging.getLogger().setLevel(log_level)
|
|
try:
|
|
func(*args, **kwargs)
|
|
finally:
|
|
logging.getLogger().setLevel(restore_log_level)
|
|
return
|
|
|
|
with osutils.TempDir() as tempdir:
|
|
self._stdout_path = os.path.join(tempdir, STDOUT_FILE)
|
|
self._stderr_path = os.path.join(tempdir, STDERR_FILE)
|
|
osutils.Touch(self._stdout_path)
|
|
osutils.Touch(self._stderr_path)
|
|
try:
|
|
with parallel.BackgroundTaskRunner(
|
|
self.CaptureOutputInBackground, func, *args, **kwargs) as queue:
|
|
queue.put([])
|
|
self.OpenStdoutStderr()
|
|
while True:
|
|
self.ParseOutput()
|
|
if self.WaitUntilComplete(update_period):
|
|
break
|
|
# Before we exit, parse the output again to update progress bar.
|
|
self.ParseOutput()
|
|
# Final sanity check to update the progress bar to 100% if it was used
|
|
# by ParseOutput
|
|
self.Cleanup()
|
|
except:
|
|
# Add a blank line before the logging message so the message isn't
|
|
# touching the progress bar.
|
|
sys.stdout.write('\n')
|
|
logging.error('Oops. Something went wrong.')
|
|
# Raise the exception so it can be caught again.
|
|
raise
|
|
|
|
|
|
class ParallelEmergeOperation(ProgressBarOperation):
|
|
"""ProgressBarOperation specific for scripts/parallel_emerge.py."""
|
|
|
|
def __init__(self):
|
|
super(ParallelEmergeOperation, self).__init__()
|
|
self._total = None
|
|
self._completed = 0
|
|
self._printed_no_packages = False
|
|
self._events = ['Fetched ', 'Completed ']
|
|
self._msg = None
|
|
|
|
def _GetTotal(self, output):
|
|
"""Get total packages by looking for Total: digits packages."""
|
|
match = re.search(r'Total: (\d+) packages', output)
|
|
return int(match.group(1)) if match else None
|
|
|
|
def SetProgressBarMessage(self, msg):
|
|
"""Message to be shown before the progress bar is displayed with 0%.
|
|
|
|
The message is not displayed if the progress bar is not going to be
|
|
displayed.
|
|
"""
|
|
self._msg = msg
|
|
|
|
def ParseOutput(self, output=None):
|
|
"""Parse the output of emerge to determine how to update progress bar.
|
|
|
|
1) Figure out how many packages exist. If the total number of packages to be
|
|
built is zero, then we do not display the progress bar.
|
|
2) Whenever a package is downloaded or built, 'Fetched' and 'Completed' are
|
|
printed respectively. By counting counting 'Fetched's and 'Completed's, we
|
|
can determine how much to update the progress bar by.
|
|
|
|
Args:
|
|
output: Pass in output to parse instead of reading from self._stdout and
|
|
self._stderr.
|
|
|
|
Returns:
|
|
A fraction between 0 and 1 indicating the level of the progress bar. If
|
|
the progress bar isn't displayed, then the return value is -1.
|
|
"""
|
|
if output is None:
|
|
stdout = self._stdout.read()
|
|
stderr = self._stderr.read()
|
|
output = stdout + stderr
|
|
|
|
if self._total is None:
|
|
temp = self._GetTotal(output)
|
|
if temp is not None:
|
|
self._total = temp * len(self._events)
|
|
if self._msg is not None:
|
|
logging.notice(self._msg)
|
|
|
|
for event in self._events:
|
|
self._completed += output.count(event)
|
|
|
|
if not self._printed_no_packages and self._total == 0:
|
|
logging.notice('No packages to build.')
|
|
self._printed_no_packages = True
|
|
|
|
if self._total:
|
|
progress = self._completed / self._total
|
|
self.ProgressBar(progress)
|
|
return progress
|
|
else:
|
|
return -1
|
|
|
|
|
|
# TODO(sjg): When !isatty(), keep stdout and stderr separate so they can be
|
|
# redirected separately
|
|
# TODO(sjg): Add proper docs to this fileno
|
|
# TODO(sjg): Handle stdin wait in quite mode, rather than silently stalling
|
|
|
|
class Operation(object):
|
|
"""Class which controls stdio and progress of an operation in progress.
|
|
|
|
This class is created to handle stdio for a running subprocess. It filters
|
|
it looking for errors and progress information. Optionally it can output the
|
|
stderr and stdout to the terminal, but it is normally supressed.
|
|
|
|
Progress information is garnered from the subprocess output based on
|
|
knowledge of the legacy scripts, but at some point will move over to using
|
|
real progress information reported through new python methods which will
|
|
replace the scripts.
|
|
|
|
Each operation has a name, and this class handles displaying this name
|
|
as it reports progress.
|
|
|
|
Operation Objects
|
|
=================
|
|
|
|
verbose: True / False
|
|
In verbose mode all output from subprocesses is displayed, otherwise
|
|
this output is normally supressed, unless we think it indicates an error.
|
|
|
|
progress: True / False
|
|
The output from subprocesses can be analysed in a very basic manner to
|
|
try to present progress information to the user.
|
|
|
|
explicit_verbose: True / False
|
|
False if we are not just using default verbosity. In that case we allow
|
|
verbosity to be enabled on request, since the user has not explicitly
|
|
disabled it. This is used by commands that the user issues with the
|
|
expectation that output would ordinarily be visible.
|
|
"""
|
|
|
|
def __init__(self, name, color=None):
|
|
"""Create a new operation.
|
|
|
|
Args:
|
|
name: Operation name in a form to be displayed for the user.
|
|
color: Determines policy for sending color to stdout; see terminal.Color
|
|
for details on interpretation on the value.
|
|
"""
|
|
self._name = name # Operation name.
|
|
self.verbose = False # True to echo subprocess output.
|
|
self.progress = True # True to report progress of the operation
|
|
self._column = 0 # Current output column (always 0 unless verbose).
|
|
self._update_len = 0 # Length of last progress update message.
|
|
self._line = '' # text of current line, so far
|
|
self.explicit_verbose = False
|
|
|
|
self._color = Color(enabled=color)
|
|
|
|
# -1 = no newline pending
|
|
# n = newline pending, and line length of last line was n
|
|
self._pending_nl = -1
|
|
|
|
# the type of the last stream to emit data on the current lines
|
|
# can be sys.stdout, sys.stderr (both from the subprocess), or None
|
|
# for our own mesages
|
|
self._cur_stream = None
|
|
|
|
self._error_count = 0 # number of error lines we have reported
|
|
|
|
def __del__(self):
|
|
"""Object is about to be destroyed, so finish out output cleanly."""
|
|
self.FinishOutput()
|
|
|
|
def FinishOutput(self):
|
|
"""Finish off any pending output.
|
|
|
|
This finishes any output line currently in progress and resets the color
|
|
back to normal.
|
|
"""
|
|
self._FinishLine(self.verbose, final=True)
|
|
if self._column and self.verbose:
|
|
print(self._color.Stop())
|
|
self._column = 0
|
|
|
|
def WereErrorsDetected(self):
|
|
"""Returns whether any errors have been detected.
|
|
|
|
Returns:
|
|
True if any errors have been detected in subprocess output so far.
|
|
False otherwise
|
|
"""
|
|
return self._error_count > 0
|
|
|
|
def SetName(self, name):
|
|
"""Set the name of the operation as displayed to the user.
|
|
|
|
Args:
|
|
name: Operation name.
|
|
"""
|
|
self._name = name
|
|
|
|
def _FilterOutputForErrors(self, line, print_error):
|
|
"""Filter a line of output to look for and display errors.
|
|
|
|
This uses a few regular expression searches to spot common error reports
|
|
from subprocesses. A count of these is kept so we know how many occurred.
|
|
Optionally they are displayed in red on the terminal.
|
|
|
|
Args:
|
|
line: the output line to filter, as a string.
|
|
print_error: True to print the error, False to just record it.
|
|
"""
|
|
bad_things = ['Cannot GET', 'ERROR', '!!!', 'FAILED']
|
|
for bad_thing in bad_things:
|
|
if re.search(bad_thing, line, flags=re.IGNORECASE):
|
|
self._error_count += 1
|
|
if print_error:
|
|
print(self._color.Color(self._color.RED, line))
|
|
break
|
|
|
|
def _FilterOutputForProgress(self, line):
|
|
"""Filter a line of output to look for and dispay progress information.
|
|
|
|
This uses a simple regular expression search to spot progress information
|
|
coming from subprocesses. This is sent to the _Progress() method.
|
|
|
|
Args:
|
|
line: the output line to filter, as a string.
|
|
"""
|
|
match = re.match(r'Pending (\d+).*Total (\d+)', line)
|
|
if match:
|
|
pending = int(match.group(1))
|
|
total = int(match.group(2))
|
|
self._Progress(total - pending, total)
|
|
|
|
def _Progress(self, upto, total):
|
|
"""Record and optionally display progress information.
|
|
|
|
Args:
|
|
upto: which step we are up to in the operation (integer, from 0).
|
|
total: total number of steps in operation,
|
|
"""
|
|
if total > 0:
|
|
update_str = '%s...%d%% (%d of %d)' % (self._name,
|
|
upto * 100 // total, upto, total)
|
|
if self.progress:
|
|
# Finish the current line, print progress, and remember its length.
|
|
self._FinishLine(self.verbose)
|
|
|
|
# Sometimes the progress string shrinks and in this case we need to
|
|
# blank out the characters at the end of the line that will not be
|
|
# overwritten by the new line
|
|
pad = max(self._update_len - len(update_str), 0)
|
|
sys.stdout.write(update_str + (' ' * pad) + '\r')
|
|
self._update_len = len(update_str)
|
|
|
|
def _FinishLine(self, display, final=False):
|
|
"""Finish off the current line and prepare to start a new one.
|
|
|
|
If a new line is pending from the previous line, then this will be output,
|
|
along with a color reset if needed.
|
|
|
|
We also handle removing progress messages from the output. This is done
|
|
using a carriage return character, following by spaces.
|
|
|
|
Args:
|
|
display: True to display output, False to suppress it
|
|
final: True if this is the final output before we exit, in which case
|
|
we must clean up any remaining progress message by overwriting
|
|
it with spaces, then carriage return
|
|
"""
|
|
if display:
|
|
if self._pending_nl != -1:
|
|
# If out last output line was shorter than the progress info
|
|
# add spaces.
|
|
if self._pending_nl < self._update_len:
|
|
print(' ' * (self._update_len - self._pending_nl), end='')
|
|
|
|
# Output the newline, and reset our counter.
|
|
sys.stdout.write(self._color.Stop())
|
|
print()
|
|
|
|
# If this is the last thing that this operation will print, we need to
|
|
# close things off. So if there is some text on the current line but not
|
|
# enough to overwrite all the progress information we have sent, add some
|
|
# more spaces.
|
|
if final and self._update_len:
|
|
print(' ' * self._update_len, '\r', end='')
|
|
|
|
self._pending_nl = -1
|
|
|
|
def _CheckStreamAndColor(self, stream, display):
|
|
"""Check that we're writing to the same stream as last call. No? New line.
|
|
|
|
If starting a new line, set the color correctly:
|
|
stdout Magenta
|
|
stderr Red
|
|
other White / no colors
|
|
|
|
Args:
|
|
stream: The stream we're going to write to.
|
|
display: True to display it on terms, False to suppress it.
|
|
"""
|
|
if self._column > 0 and stream != self._cur_stream:
|
|
self._FinishLine(display)
|
|
if display:
|
|
print(self._color.Stop())
|
|
|
|
self._column = 0
|
|
self._line = ''
|
|
|
|
# Use colors for child output.
|
|
if self._column == 0:
|
|
self._FinishLine(display)
|
|
if display:
|
|
color = None
|
|
if stream == sys.stdout:
|
|
color = self._color.MAGENTA
|
|
elif stream == sys.stderr:
|
|
color = self._color.RED
|
|
if color:
|
|
sys.stdout.write(self._color.Start(color))
|
|
|
|
self._cur_stream = stream
|
|
|
|
def _Out(self, stream, text, display, newline=False, do_output_filter=True):
|
|
"""Output some text received from a child, or generated internally.
|
|
|
|
This method is the guts of the Operation class since it understands how to
|
|
convert a series of output requests on different streams into something
|
|
coherent for the user.
|
|
|
|
If the stream has changed, then a new line is started even if we were
|
|
still halfway through the previous line. This prevents stdout and stderr
|
|
becoming mixed up quite so badly.
|
|
|
|
We use color to indicate lines which are stdout and stderr. If the output
|
|
received from the child has color codes in it already, we pass these
|
|
through, so our colors can be overridden. If output is redirected then we
|
|
do not add color by default. Note that nothing stops the child from adding
|
|
it, but since we present ourselves as a terminal to the child, one might
|
|
hope that the child will not generate color.
|
|
|
|
If display is False, then we will not actually send this text to the
|
|
terminal. This is uses when verbose is required to be False.
|
|
|
|
Args:
|
|
stream: stream on which the text was received:
|
|
sys.stdout - received on stdout
|
|
sys.stderr - received on stderr
|
|
None - generated by us / internally
|
|
text: text to output
|
|
display: True to display it on terms, False to suppress it
|
|
newline: True to start a new line after this text, False to put the next
|
|
lot of output immediately after this.
|
|
do_output_filter: True to look through output for errors and progress.
|
|
"""
|
|
self._CheckStreamAndColor(stream, display)
|
|
|
|
# Output what we have, and remember what column we are up to.
|
|
if display:
|
|
sys.stdout.write(text)
|
|
self._column += len(text)
|
|
# If a newline is required, remember to output it later.
|
|
if newline:
|
|
self._pending_nl = self._column
|
|
self._column = 0
|
|
|
|
self._line += text
|
|
|
|
# If we now have a whole line, check it for errors and progress.
|
|
if newline:
|
|
if do_output_filter:
|
|
self._FilterOutputForErrors(self._line, print_error=not display)
|
|
self._FilterOutputForProgress(self._line)
|
|
self._line = ''
|
|
|
|
def Output(self, stream, data):
|
|
r"""Handle the output of a block of text from the subprocess.
|
|
|
|
All subprocess output should be sent through this method. It is split into
|
|
lines which are processed separately using the _Out() method.
|
|
|
|
Args:
|
|
stream: Which file the output come in on:
|
|
sys.stdout: stdout
|
|
sys.stderr: stderr
|
|
None: Our own internal output
|
|
data: Output data as a big string, potentially containing many lines of
|
|
text. Each line should end with \r\n. There is no requirement to send
|
|
whole lines - this method happily handles fragments and tries to
|
|
present then to the user as early as possible
|
|
|
|
#TODO(sjg): Just use a list as the input parameter to avoid the split.
|
|
"""
|
|
# We cannot use splitlines() here as we need this exact behavior
|
|
lines = data.split('\r\n')
|
|
|
|
# Output each full line, with a \n after it.
|
|
for line in lines[:-1]:
|
|
self._Out(stream, line, display=self.verbose, newline=True)
|
|
|
|
# If we have a partial line at the end, output what we have.
|
|
# We will continue it later.
|
|
if lines[-1]:
|
|
self._Out(stream, lines[-1], display=self.verbose)
|
|
|
|
# Flush so that the terminal will receive partial line output (now!)
|
|
sys.stdout.flush()
|
|
|
|
def Outline(self, line):
|
|
r"""Output a line of text to the display.
|
|
|
|
This outputs text generated internally, such as a warning message or error
|
|
summary. It ensures that our message plays nicely with child output if
|
|
any.
|
|
|
|
Args:
|
|
line: text to output (without \n on the end)
|
|
"""
|
|
self._Out(None, line, display=True, newline=True)
|
|
self._FinishLine(display=True)
|
|
|
|
def Info(self, line):
|
|
r"""Output a line of information text to the display in verbose mode.
|
|
|
|
Args:
|
|
line: text to output (without \n on the end)
|
|
"""
|
|
self._Out(None, self._color.Color(self._color.BLUE, line),
|
|
display=self.verbose, newline=True, do_output_filter=False)
|
|
self._FinishLine(display=True)
|
|
|
|
def Notice(self, line):
|
|
r"""Output a line of notification text to the display.
|
|
|
|
Args:
|
|
line: text to output (without \n on the end)
|
|
"""
|
|
self._Out(None, self._color.Color(self._color.GREEN, line),
|
|
display=True, newline=True, do_output_filter=False)
|
|
self._FinishLine(display=True)
|
|
|
|
def Warning(self, line):
|
|
r"""Output a line of warning text to the display.
|
|
|
|
Args:
|
|
line: text to output (without \n on the end)
|
|
"""
|
|
self._Out(None, self._color.Color(self._color.YELLOW, line),
|
|
display=True, newline=True, do_output_filter=False)
|
|
self._FinishLine(display=True)
|
|
|
|
def Error(self, line):
|
|
r"""Output a line of error text to the display.
|
|
|
|
Args:
|
|
line: text to output (without \n on the end)
|
|
"""
|
|
self._Out(None, self._color.Color(self._color.RED, line),
|
|
display=True, newline=True, do_output_filter=False)
|
|
self._FinishLine(display=True)
|
|
|
|
def Die(self, line):
|
|
r"""Output a line of error text to the display and die.
|
|
|
|
Args:
|
|
line: text to output (without \n on the end)
|
|
"""
|
|
self.Error(line)
|
|
sys.exit(1)
|
|
|
|
@contextlib.contextmanager
|
|
def RequestVerbose(self, request):
|
|
"""Perform something in verbose mode if the user hasn't disallowed it
|
|
|
|
This is intended to be used with something like:
|
|
|
|
with oper.RequestVerbose(True):
|
|
... do some things that generate output
|
|
|
|
Args:
|
|
request: True to request verbose mode if available, False to do nothing.
|
|
"""
|
|
old_verbose = self.verbose
|
|
if request and not self.explicit_verbose:
|
|
self.verbose = True
|
|
try:
|
|
yield
|
|
finally:
|
|
self.verbose = old_verbose
|