Repository: spack/spack

Issue:
Changes in behavior of spack install
@alalazo Can you take a look at this? There's a good chance one or both of these are related to #1186.

1. Previously, when I ran `spack install xyz`, if I pressed <kbd>v</kbd>, it would toggle back and forth to verbose mode. It no longer does this.
2. Previously, when an installation crashed, it would print out the contents of the `package.py` and point to where it crashed. Now it prints out both the `package.py` and the method that calls it. The latter is quite long, and requires the user to scroll up pretty far to find the `spack-build.out`. I don't think we need the latter.

Changes in behavior of spack install
@alalazo Can you take a look at this? There's a good chance one or both of these are related to #1186.

1. Previously, when I ran `spack install xyz`, if I pressed <kbd>v</kbd>, it would toggle back and forth to verbose mode. It no longer does this.
2. Previously, when an installation crashed, it would print out the contents of the `package.py` and point to where it crashed. Now it prints out both the `package.py` and the method that calls it. The latter is quite long, and requires the user to scroll up pretty far to find the `spack-build.out`. I don't think we need the latter.



Discussion:
I'll try to have a look as soon as possible. I think they ARE both due to #1186 and the way the forked processes are handled.

Take your time, no rush.

Can you check if #2143 solves point 2. ?

I am near to a solution for point 1, but need to sort out a couple of details. Basically I need to forward :

```
input_stream = os.fdopen(os.dup(sys.stdin.fileno()))
```

down to child processes. 

@tgamblin @adamjstewart How urgent do you consider this regression ?

Not urgent at all. It's a cool feature, but certainly not a show stopper. Doesn't need to make it into 0.10 as far as I'm concerned.

I'll try to have a look as soon as possible. I think they ARE both due to #1186 and the way the forked processes are handled.

Take your time, no rush.

Can you check if #2143 solves point 2. ?

I am near to a solution for point 1, but need to sort out a couple of details. Basically I need to forward :

```
input_stream = os.fdopen(os.dup(sys.stdin.fileno()))
```

down to child processes. 

@tgamblin @adamjstewart How urgent do you consider this regression ?

Not urgent at all. It's a cool feature, but certainly not a show stopper. Doesn't need to make it into 0.10 as far as I'm concerned.


Resolving patch:
diff --git a/lib/spack/llnl/util/tty/log.py b/lib/spack/llnl/util/tty/log.py
--- a/lib/spack/llnl/util/tty/log.py
+++ b/lib/spack/llnl/util/tty/log.py
@@ -120,7 +120,14 @@ class log_output(object):
     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
         # Various output options
         self.echo = echo
@@ -132,46 +139,57 @@ def __init__(self, filename, echo=False, force_color=False, debug=False):
         self.directAssignment = False
         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
         self.parent_pipe, self.child_pipe = multiprocessing.Pipe()
+        # Input stream that controls verbosity interactively
+        self.input_stream = input_stream
 
     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)
 
     def __exit__(self, exc_type, exc_val, exc_tb):
         self.parent_pipe.send(True)
         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.
         read_file = os.fdopen(read, 'r', 0)
         with open(self.filename, 'w') as log_file:
-            with keyboard_input(sys.stdin):
+            with keyboard_input(input_stream):
                 while True:
-                    rlist, _, _ = select.select([read_file, sys.stdin], [], [])
-                    if not rlist:
-                        break
+                    # Without the last parameter (timeout) select will wait
+                    # until at least one of the two streams are ready. This
+                    # may cause the function to hang.
+                    rlist, _, _ = select.select(
+                        [read_file, input_stream], [], [], 0
+                    )
 
                     # Allow user to toggle echo with 'v' key.
                     # Currently ignores other chars.
-                    if sys.stdin in rlist:
-                        if sys.stdin.read(1) == 'v':
+                    if input_stream in rlist:
+                        if input_stream.read(1) == 'v':
                             self.echo = not self.echo
 
                     # Handle output from the with block process.
                     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()
-                        if not line:
-                            # For some reason we never reach this point...
-                            break
 
                         # Echo to stdout if requested.
                         if self.echo:
diff --git a/lib/spack/spack/build_environment.py b/lib/spack/spack/build_environment.py
--- a/lib/spack/spack/build_environment.py
+++ b/lib/spack/spack/build_environment.py
@@ -528,10 +528,10 @@ def child_fun():
     carries on.
     """
 
-    def child_execution(child_connection):
+    def child_execution(child_connection, input_stream):
         try:
             setup_package(pkg, dirty=dirty)
-            function()
+            function(input_stream)
             child_connection.send(None)
         except:
             # catch ANYTHING that goes wrong in the child process
@@ -559,11 +559,18 @@ def child_execution(child_connection):
             child_connection.close()
 
     parent_connection, child_connection = multiprocessing.Pipe()
-    p = multiprocessing.Process(
-        target=child_execution,
-        args=(child_connection,)
-    )
-    p.start()
+    try:
+        # Forward sys.stdin to be able to activate / deactivate
+        # verbosity pressing a key at run-time
+        input_stream = os.fdopen(os.dup(sys.stdin.fileno()))
+        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()
     p.join()
 
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -95,8 +95,6 @@ def __get__(self, instance, owner):
         # install phase, thus return a properly set wrapper
         phase = getattr(instance, self.name)
 
-        print phase
-
         @functools.wraps(phase)
         def phase_wrapper(spec, prefix):
             # 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
     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.
-    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
     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
     install function.
 
-    For a full list of commands and variables available in module scope, see the
-    add_commands_to_module() function in this class. This is where most of
-    them are created and set on the module.
+    For a full list of commands and variables available in module scope, see
+    the add_commands_to_module() function in this class. This is where most
+    of them are created and set on the module.
 
     **Parallel Builds**
 
@@ -1197,7 +1196,7 @@ def do_install(self,
         self.make_jobs = make_jobs
 
         # Then install the package itself.
-        def build_process():
+        def build_process(input_stream):
             """Forked for each build. Has its own process and python
                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
                     # everything to log_path
                     redirection_context = log_output(
-                        log_path, verbose,
-                        sys.stdout.isatty(),
-                        True
+                        log_path,
+                        echo=verbose,
+                        force_color=sys.stdout.isatty(),
+                        debug=True,
+                        input_stream=input_stream
                     )
                     with redirection_context as log_redirection:
                         for phase_name, phase in zip(self.phases, self._InstallPhase_phases):  # NOQA: ignore=E501


Test patch: