Preserve verbosity across installs when 'v' is pressed.
- 'v' toggle was previously only good for the current install. - subsequent installs needed user to press 'v' again. - 'v' state is now preserved across dependency installs.
This commit is contained in:
parent
48440766df
commit
11196e7b69
3 changed files with 153 additions and 60 deletions
|
@ -29,6 +29,7 @@
|
||||||
import re
|
import re
|
||||||
import select
|
import select
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
xon, xoff = '\x11\n', '\x13\n'
|
xon, xoff = '\x11\n', '\x13\n'
|
||||||
control = re.compile('(\x11\n|\x13\n)')
|
control = re.compile('(\x11\n|\x13\n)')
|
||||||
|
|
||||||
|
|
||||||
def _strip(line):
|
def _strip(line):
|
||||||
"""Strip color and control characters from a line."""
|
"""Strip color and control characters from a line."""
|
||||||
return _escape.sub('', line)
|
return _escape.sub('', line)
|
||||||
|
@ -76,6 +78,9 @@ def __init__(self, stream):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
stream (file-like): stream on which to accept keyboard input
|
stream (file-like): stream on which to accept keyboard input
|
||||||
|
|
||||||
|
Note that stream can be None, in which case ``keyboard_input``
|
||||||
|
will do nothing.
|
||||||
"""
|
"""
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
|
|
||||||
|
@ -88,7 +93,7 @@ def __enter__(self):
|
||||||
self.old_cfg = None
|
self.old_cfg = None
|
||||||
|
|
||||||
# Ignore all this if the input stream is not a tty.
|
# Ignore all this if the input stream is not a tty.
|
||||||
if not self.stream.isatty():
|
if not self.stream or not self.stream.isatty():
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -119,6 +124,30 @@ def __exit__(self, exc_type, exception, traceback):
|
||||||
self.stream.fileno(), termios.TCSADRAIN, self.old_cfg)
|
self.stream.fileno(), termios.TCSADRAIN, self.old_cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def _file_descriptors_work():
|
||||||
|
"""Whether we can get file descriptors for stdout and stderr.
|
||||||
|
|
||||||
|
This tries to call ``fileno()`` on ``sys.stdout`` and ``sys.stderr``
|
||||||
|
and returns ``False`` if anything goes wrong.
|
||||||
|
|
||||||
|
This can happen, when, e.g., the test framework replaces stdout with
|
||||||
|
a ``StringIO`` object.
|
||||||
|
|
||||||
|
We have to actually try this to see whether it works, rather than
|
||||||
|
checking for the fileno attribute, beacuse frameworks like pytest add
|
||||||
|
dummy fileno methods on their dummy file objects that return
|
||||||
|
``UnsupportedOperationErrors``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# test whether we can get fds for out and error
|
||||||
|
try:
|
||||||
|
sys.stdout.fileno()
|
||||||
|
sys.stderr.fileno()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class log_output(object):
|
class log_output(object):
|
||||||
"""Context manager that logs its output to a file.
|
"""Context manager that logs its output to a file.
|
||||||
|
|
||||||
|
@ -135,7 +164,7 @@ class log_output(object):
|
||||||
# do things ... output will be logged and printed out
|
# do things ... output will be logged and printed out
|
||||||
|
|
||||||
And, if you just want to echo *some* stuff from the parent, use
|
And, if you just want to echo *some* stuff from the parent, use
|
||||||
``force_echo``:
|
``force_echo``::
|
||||||
|
|
||||||
with log_output('logfile.txt', echo=False) as logger:
|
with log_output('logfile.txt', echo=False) as logger:
|
||||||
# do things ... output will be logged
|
# do things ... output will be logged
|
||||||
|
@ -155,7 +184,7 @@ class log_output(object):
|
||||||
work within test frameworks like nose and pytest.
|
work within test frameworks like nose and pytest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, filename, echo=False, debug=False):
|
def __init__(self, filename=None, echo=False, debug=False):
|
||||||
"""Create a new output log context manager.
|
"""Create a new output log context manager.
|
||||||
|
|
||||||
Logger daemon is not started until ``__enter__()``.
|
Logger daemon is not started until ``__enter__()``.
|
||||||
|
@ -166,10 +195,38 @@ def __init__(self, filename, echo=False, debug=False):
|
||||||
|
|
||||||
self._active = False # used to prevent re-entry
|
self._active = False # used to prevent re-entry
|
||||||
|
|
||||||
|
def __call__(self, filename=None, echo=None, debug=None):
|
||||||
|
"""Thie behaves the same as init. It allows a logger to be reused.
|
||||||
|
|
||||||
|
With the ``__call__`` function, you can save state between uses
|
||||||
|
of a single logger. This is useful if you want to remember,
|
||||||
|
e.g., the echo settings for a prior ``with log_output()``::
|
||||||
|
|
||||||
|
logger = log_output()
|
||||||
|
|
||||||
|
with logger('foo.txt'):
|
||||||
|
# log things; user can change echo settings with 'v'
|
||||||
|
|
||||||
|
with logger('bar.txt'):
|
||||||
|
# log things; logger remembers prior echo settings.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if filename is not None:
|
||||||
|
self.filename = filename
|
||||||
|
if echo is not None:
|
||||||
|
self.echo = echo
|
||||||
|
if debug is not None:
|
||||||
|
self.debug = debug
|
||||||
|
return self
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if self._active:
|
if self._active:
|
||||||
raise RuntimeError("Can't re-enter the same log_output!")
|
raise RuntimeError("Can't re-enter the same log_output!")
|
||||||
|
|
||||||
|
if self.filename is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"filename must be set by either __init__ or __call__")
|
||||||
|
|
||||||
# record parent color settings before redirecting. We do this
|
# record parent color settings before redirecting. We do this
|
||||||
# because color output depends on whether the *original* stdout
|
# because color output depends on whether the *original* stdout
|
||||||
# is a TTY. New stdout won't be a TTY so we force colorization.
|
# is a TTY. New stdout won't be a TTY so we force colorization.
|
||||||
|
@ -183,10 +240,17 @@ def __enter__(self):
|
||||||
# OS-level pipe for redirecting output to logger
|
# OS-level pipe for redirecting output to logger
|
||||||
self.read_fd, self.write_fd = os.pipe()
|
self.read_fd, self.write_fd = os.pipe()
|
||||||
|
|
||||||
|
# Multiprocessing pipe for communication back from the daemon
|
||||||
|
# Currently only used to save echo value between uses
|
||||||
|
self.parent, self.child = multiprocessing.Pipe()
|
||||||
|
|
||||||
# Sets a daemon that writes to file what it reads from a pipe
|
# Sets a daemon that writes to file what it reads from a pipe
|
||||||
try:
|
try:
|
||||||
# need to pass this b/c multiprocessing closes stdin in child.
|
# need to pass this b/c multiprocessing closes stdin in child.
|
||||||
|
try:
|
||||||
input_stream = os.fdopen(os.dup(sys.stdin.fileno()))
|
input_stream = os.fdopen(os.dup(sys.stdin.fileno()))
|
||||||
|
except:
|
||||||
|
input_stream = None # just don't forward input if this fails
|
||||||
|
|
||||||
self.process = multiprocessing.Process(
|
self.process = multiprocessing.Process(
|
||||||
target=self._writer_daemon, args=(input_stream,))
|
target=self._writer_daemon, args=(input_stream,))
|
||||||
|
@ -195,6 +259,7 @@ def __enter__(self):
|
||||||
os.close(self.read_fd) # close in the parent process
|
os.close(self.read_fd) # close in the parent process
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
if input_stream:
|
||||||
input_stream.close()
|
input_stream.close()
|
||||||
|
|
||||||
# Flush immediately before redirecting so that anything buffered
|
# Flush immediately before redirecting so that anything buffered
|
||||||
|
@ -203,8 +268,8 @@ def __enter__(self):
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
# Now do the actual output rediction.
|
# Now do the actual output rediction.
|
||||||
self.use_fds = True
|
self.use_fds = _file_descriptors_work()
|
||||||
try:
|
if self.use_fds:
|
||||||
# We try first to use OS-level file descriptors, as this
|
# We try first to use OS-level file descriptors, as this
|
||||||
# redirects output for subprocesses and system calls.
|
# redirects output for subprocesses and system calls.
|
||||||
|
|
||||||
|
@ -217,13 +282,11 @@ def __enter__(self):
|
||||||
os.dup2(self.write_fd, sys.stderr.fileno())
|
os.dup2(self.write_fd, sys.stderr.fileno())
|
||||||
os.close(self.write_fd)
|
os.close(self.write_fd)
|
||||||
|
|
||||||
except AttributeError:
|
else:
|
||||||
# Using file descriptors can fail if stdout and stderr don't
|
# Handle I/O the Python way. This won't redirect lower-level
|
||||||
# have a fileno attribute. This can happen, when, e.g., the
|
# output, but it's the best we can do, and the caller
|
||||||
# test framework replaces stdout with a StringIO object. We
|
# shouldn't expect any better, since *they* have apparently
|
||||||
# handle thi the Python way. This won't redirect lower-level
|
# redirected I/O the Python way.
|
||||||
# output, but it's the best we can do.
|
|
||||||
self.use_fds = False
|
|
||||||
|
|
||||||
# Save old stdout and stderr file objects
|
# Save old stdout and stderr file objects
|
||||||
self._saved_stdout = sys.stdout
|
self._saved_stdout = sys.stdout
|
||||||
|
@ -262,6 +325,9 @@ def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
sys.stdout = self._saved_stdout
|
sys.stdout = self._saved_stdout
|
||||||
sys.stderr = self._saved_stderr
|
sys.stderr = self._saved_stderr
|
||||||
|
|
||||||
|
# recover and store echo settings from the child before it dies
|
||||||
|
self.echo = self.parent.recv()
|
||||||
|
|
||||||
# join the daemon process. The daemon will quit automatically
|
# join the daemon process. The daemon will quit automatically
|
||||||
# when the write pipe is closed; we just wait for it here.
|
# when the write pipe is closed; we just wait for it here.
|
||||||
self.process.join()
|
self.process.join()
|
||||||
|
@ -299,14 +365,17 @@ def _writer_daemon(self, stdin):
|
||||||
echo = self.echo # initial echo setting, user-controllable
|
echo = self.echo # initial echo setting, user-controllable
|
||||||
force_echo = False # parent can force echo for certain output
|
force_echo = False # parent can force echo for certain output
|
||||||
|
|
||||||
|
# list of streams to select from
|
||||||
|
istreams = [in_pipe, stdin] if stdin else [in_pipe]
|
||||||
|
|
||||||
|
try:
|
||||||
with open(self.filename, 'w') as log_file:
|
with open(self.filename, 'w') as log_file:
|
||||||
with keyboard_input(stdin):
|
with keyboard_input(stdin):
|
||||||
while True:
|
while True:
|
||||||
# Without the last parameter (timeout) select will
|
# Without the last parameter (timeout) select will
|
||||||
# wait until at least one of the two streams are
|
# wait until at least one of the two streams are
|
||||||
# ready. This may cause the function to hang.
|
# ready. This may cause the function to hang.
|
||||||
rlist, _, xlist = select.select(
|
rlist, _, xlist = select.select(istreams, [], [], 0)
|
||||||
[in_pipe, stdin], [], [], 0)
|
|
||||||
|
|
||||||
# Allow user to toggle echo with 'v' key.
|
# Allow user to toggle echo with 'v' key.
|
||||||
# Currently ignores other chars.
|
# Currently ignores other chars.
|
||||||
|
@ -339,3 +408,10 @@ def _writer_daemon(self, stdin):
|
||||||
force_echo = True
|
force_echo = True
|
||||||
if xoff in controls:
|
if xoff in controls:
|
||||||
force_echo = False
|
force_echo = False
|
||||||
|
|
||||||
|
except:
|
||||||
|
tty.error("Exception occurred in writer daemon!")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# send echo value back to the parent so it can be preserved.
|
||||||
|
self.child.send(echo)
|
||||||
|
|
|
@ -550,8 +550,8 @@ def child_process(child_pipe, input_stream):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setup_package(pkg, dirty=dirty)
|
setup_package(pkg, dirty=dirty)
|
||||||
function()
|
return_value = function()
|
||||||
child_pipe.send(None)
|
child_pipe.send(return_value)
|
||||||
except StopIteration as e:
|
except StopIteration as e:
|
||||||
# StopIteration is used to stop installations
|
# StopIteration is used to stop installations
|
||||||
# before the final stage, mainly for debug purposes
|
# before the final stage, mainly for debug purposes
|
||||||
|
@ -598,11 +598,12 @@ def child_process(child_pipe, input_stream):
|
||||||
if input_stream is not None:
|
if input_stream is not None:
|
||||||
input_stream.close()
|
input_stream.close()
|
||||||
|
|
||||||
child_exc = parent_pipe.recv()
|
child_result = parent_pipe.recv()
|
||||||
p.join()
|
p.join()
|
||||||
|
|
||||||
if child_exc is not None:
|
if isinstance(child_result, ChildError):
|
||||||
raise child_exc
|
raise child_result
|
||||||
|
return child_result
|
||||||
|
|
||||||
|
|
||||||
def get_package_context(traceback):
|
def get_package_context(traceback):
|
||||||
|
|
|
@ -542,6 +542,9 @@ class SomePackage(Package):
|
||||||
#: Defaults to the empty string.
|
#: Defaults to the empty string.
|
||||||
license_url = ''
|
license_url = ''
|
||||||
|
|
||||||
|
# Verbosity level, preserved across installs.
|
||||||
|
_verbose = None
|
||||||
|
|
||||||
def __init__(self, spec):
|
def __init__(self, spec):
|
||||||
# this determines how the package should be built.
|
# this determines how the package should be built.
|
||||||
self.spec = spec
|
self.spec = spec
|
||||||
|
@ -1275,8 +1278,13 @@ def do_install(self,
|
||||||
|
|
||||||
# Then install the package itself.
|
# Then install the package itself.
|
||||||
def build_process():
|
def build_process():
|
||||||
"""Forked for each build. Has its own process and python
|
"""This implements the process forked for each build.
|
||||||
module space set up by build_environment.fork()."""
|
|
||||||
|
Has its own process and python module space set up by
|
||||||
|
build_environment.fork().
|
||||||
|
|
||||||
|
This function's return value is returned to the parent process.
|
||||||
|
"""
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
if not fake:
|
if not fake:
|
||||||
|
@ -1289,6 +1297,11 @@ def build_process():
|
||||||
'Building {0} [{1}]'.format(self.name, self.build_system_class)
|
'Building {0} [{1}]'.format(self.name, self.build_system_class)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# get verbosity from do_install() parameter or saved value
|
||||||
|
echo = verbose
|
||||||
|
if PackageBase._verbose is not None:
|
||||||
|
echo = PackageBase._verbose
|
||||||
|
|
||||||
self.stage.keep = keep_stage
|
self.stage.keep = keep_stage
|
||||||
with self._stage_and_write_lock():
|
with self._stage_and_write_lock():
|
||||||
# Run the pre-install hook in the child process after
|
# Run the pre-install hook in the child process after
|
||||||
|
@ -1314,10 +1327,7 @@ def build_process():
|
||||||
|
|
||||||
# Spawn a daemon that reads from a pipe and redirects
|
# Spawn a daemon that reads from a pipe and redirects
|
||||||
# everything to log_path
|
# everything to log_path
|
||||||
with log_output(log_path,
|
with log_output(log_path, echo, True) as logger:
|
||||||
echo=verbose,
|
|
||||||
debug=True) as logger:
|
|
||||||
|
|
||||||
for phase_name, phase_attr in zip(
|
for phase_name, phase_attr in zip(
|
||||||
self.phases, self._InstallPhase_phases):
|
self.phases, self._InstallPhase_phases):
|
||||||
|
|
||||||
|
@ -1328,6 +1338,7 @@ def build_process():
|
||||||
phase = getattr(self, phase_attr)
|
phase = getattr(self, phase_attr)
|
||||||
phase(self.spec, self.prefix)
|
phase(self.spec, self.prefix)
|
||||||
|
|
||||||
|
echo = logger.echo
|
||||||
self.log()
|
self.log()
|
||||||
# Run post install hooks before build stage is removed.
|
# Run post install hooks before build stage is removed.
|
||||||
spack.hooks.post_install(self.spec)
|
spack.hooks.post_install(self.spec)
|
||||||
|
@ -1342,13 +1353,18 @@ def build_process():
|
||||||
_hms(self._total_time)))
|
_hms(self._total_time)))
|
||||||
print_pkg(self.prefix)
|
print_pkg(self.prefix)
|
||||||
|
|
||||||
|
# preserve verbosity across runs
|
||||||
|
return echo
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create the install prefix and fork the build process.
|
# Create the install prefix and fork the build process.
|
||||||
if not os.path.exists(self.prefix):
|
if not os.path.exists(self.prefix):
|
||||||
spack.store.layout.create_install_directory(self.spec)
|
spack.store.layout.create_install_directory(self.spec)
|
||||||
|
|
||||||
# Fork a child to do the actual installation
|
# Fork a child to do the actual installation
|
||||||
spack.build_environment.fork(self, build_process, dirty=dirty)
|
# we preserve verbosity settings across installs.
|
||||||
|
PackageBase._verbose = spack.build_environment.fork(
|
||||||
|
self, build_process, dirty=dirty)
|
||||||
|
|
||||||
# If we installed then we should keep the prefix
|
# If we installed then we should keep the prefix
|
||||||
keep_prefix = self.last_phase is None or keep_prefix
|
keep_prefix = self.last_phase is None or keep_prefix
|
||||||
|
|
Loading…
Reference in a new issue