spack install: forward sys.stdin to child processes (#2158)

* spack install: forward sys.stdin to child processes fixes #2140

 - [ ] redirection process is spawned in __enter__ instead of __init__
 - [ ] sys.stdin is forwarded to child processes

* log: wrapped __init__ definition
This commit is contained in:
Massimiliano Culpo 2016-11-03 16:03:10 +01:00 committed by Todd Gamblin
parent 5b5894afba
commit 36a4ca8b11
3 changed files with 62 additions and 36 deletions

View File

@ -120,7 +120,14 @@ class log_output(object):
daemon joining. If echo is True, also prints the output to stdout. daemon joining. If echo is True, also prints the output to stdout.
""" """
def __init__(self, filename, echo=False, force_color=False, debug=False): def __init__(
self,
filename,
echo=False,
force_color=False,
debug=False,
input_stream=sys.stdin
):
self.filename = filename self.filename = filename
# Various output options # Various output options
self.echo = echo self.echo = echo
@ -132,46 +139,57 @@ def __init__(self, filename, echo=False, force_color=False, debug=False):
self.directAssignment = False self.directAssignment = False
self.read, self.write = os.pipe() self.read, self.write = os.pipe()
# Sets a daemon that writes to file what it reads from a pipe
self.p = multiprocessing.Process(
target=self._spawn_writing_daemon,
args=(self.read,),
name='logger_daemon'
)
self.p.daemon = True
# Needed to un-summon the daemon # Needed to un-summon the daemon
self.parent_pipe, self.child_pipe = multiprocessing.Pipe() self.parent_pipe, self.child_pipe = multiprocessing.Pipe()
# Input stream that controls verbosity interactively
self.input_stream = input_stream
def __enter__(self): def __enter__(self):
self.p.start() # Sets a daemon that writes to file what it reads from a pipe
try:
fwd_input_stream = os.fdopen(
os.dup(self.input_stream.fileno())
)
self.p = multiprocessing.Process(
target=self._spawn_writing_daemon,
args=(self.read, fwd_input_stream),
name='logger_daemon'
)
self.p.daemon = True
self.p.start()
finally:
fwd_input_stream.close()
return log_output.OutputRedirection(self) return log_output.OutputRedirection(self)
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
self.parent_pipe.send(True) self.parent_pipe.send(True)
self.p.join(60.0) # 1 minute to join the child self.p.join(60.0) # 1 minute to join the child
def _spawn_writing_daemon(self, read): def _spawn_writing_daemon(self, read, input_stream):
# Parent: read from child, skip the with block. # Parent: read from child, skip the with block.
read_file = os.fdopen(read, 'r', 0) read_file = os.fdopen(read, 'r', 0)
with open(self.filename, 'w') as log_file: with open(self.filename, 'w') as log_file:
with keyboard_input(sys.stdin): with keyboard_input(input_stream):
while True: while True:
rlist, _, _ = select.select([read_file, sys.stdin], [], []) # Without the last parameter (timeout) select will wait
if not rlist: # until at least one of the two streams are ready. This
break # may cause the function to hang.
rlist, _, _ = select.select(
[read_file, input_stream], [], [], 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.
if sys.stdin in rlist: if input_stream in rlist:
if sys.stdin.read(1) == 'v': if input_stream.read(1) == 'v':
self.echo = not self.echo self.echo = not self.echo
# Handle output from the with block process. # Handle output from the with block process.
if read_file in rlist: if read_file in rlist:
# If we arrive here it means that
# read_file was ready for reading : it
# should never happen that line is false-ish
line = read_file.readline() line = read_file.readline()
if not line:
# For some reason we never reach this point...
break
# Echo to stdout if requested. # Echo to stdout if requested.
if self.echo: if self.echo:

View File

@ -528,10 +528,10 @@ def child_fun():
carries on. carries on.
""" """
def child_execution(child_connection): def child_execution(child_connection, input_stream):
try: try:
setup_package(pkg, dirty=dirty) setup_package(pkg, dirty=dirty)
function() function(input_stream)
child_connection.send(None) child_connection.send(None)
except: except:
# catch ANYTHING that goes wrong in the child process # catch ANYTHING that goes wrong in the child process
@ -559,11 +559,18 @@ def child_execution(child_connection):
child_connection.close() child_connection.close()
parent_connection, child_connection = multiprocessing.Pipe() parent_connection, child_connection = multiprocessing.Pipe()
p = multiprocessing.Process( try:
target=child_execution, # Forward sys.stdin to be able to activate / deactivate
args=(child_connection,) # verbosity pressing a key at run-time
) input_stream = os.fdopen(os.dup(sys.stdin.fileno()))
p.start() p = multiprocessing.Process(
target=child_execution,
args=(child_connection, input_stream)
)
p.start()
finally:
# Close the input stream in the parent process
input_stream.close()
child_exc = parent_connection.recv() child_exc = parent_connection.recv()
p.join() p.join()

View File

@ -95,8 +95,6 @@ def __get__(self, instance, owner):
# install phase, thus return a properly set wrapper # install phase, thus return a properly set wrapper
phase = getattr(instance, self.name) phase = getattr(instance, self.name)
print phase
@functools.wraps(phase) @functools.wraps(phase)
def phase_wrapper(spec, prefix): def phase_wrapper(spec, prefix):
# Check instance attributes at the beginning of a phase # Check instance attributes at the beginning of a phase
@ -394,7 +392,8 @@ class Stackwalker(Package):
The install function is designed so that someone not too terribly familiar The install function is designed so that someone not too terribly familiar
with Python could write a package installer. For example, we put a number with Python could write a package installer. For example, we put a number
of commands in install scope that you can use almost like shell commands. of commands in install scope that you can use almost like shell commands.
These include make, configure, cmake, rm, rmtree, mkdir, mkdirp, and others. These include make, configure, cmake, rm, rmtree, mkdir, mkdirp, and
others.
You can see above in the cmake script that these commands are used to run You can see above in the cmake script that these commands are used to run
configure and make almost like they're used on the command line. The configure and make almost like they're used on the command line. The
@ -409,9 +408,9 @@ class Stackwalker(Package):
pollute other namespaces, and it allows you to more easily implement an pollute other namespaces, and it allows you to more easily implement an
install function. install function.
For a full list of commands and variables available in module scope, see the For a full list of commands and variables available in module scope, see
add_commands_to_module() function in this class. This is where most of the add_commands_to_module() function in this class. This is where most
them are created and set on the module. of them are created and set on the module.
**Parallel Builds** **Parallel Builds**
@ -1197,7 +1196,7 @@ def do_install(self,
self.make_jobs = make_jobs self.make_jobs = make_jobs
# Then install the package itself. # Then install the package itself.
def build_process(): def build_process(input_stream):
"""Forked for each build. Has its own process and python """Forked for each build. Has its own process and python
module space set up by build_environment.fork().""" module space set up by build_environment.fork()."""
@ -1239,9 +1238,11 @@ 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
redirection_context = log_output( redirection_context = log_output(
log_path, verbose, log_path,
sys.stdout.isatty(), echo=verbose,
True force_color=sys.stdout.isatty(),
debug=True,
input_stream=input_stream
) )
with redirection_context as log_redirection: with redirection_context as log_redirection:
for phase_name, phase in zip(self.phases, self._InstallPhase_phases): # NOQA: ignore=E501 for phase_name, phase in zip(self.phases, self._InstallPhase_phases): # NOQA: ignore=E501