Skip to content
Snippets Groups Projects
Commit 3826cdf1 authored by Greg Becker's avatar Greg Becker Committed by Todd Gamblin
Browse files

multiprocessing: allow Spack to run uninterrupted in background (#14682)

Spack currently cannot run as a background process uninterrupted because some of the logging functions used in the install method (especially to create the dynamic verbosity toggle with the v key) cause the OS to issue a SIGTTOU to Spack when it's backgrounded.

This PR puts the necessary gatekeeping in place so that Spack doesn't do anything that will cause a signal to stop the process when operating as a background process.
parent 30b47045
Branches
Tags
No related merge requests found
...@@ -40,6 +40,7 @@ packages: ...@@ -40,6 +40,7 @@ packages:
pil: [py-pillow] pil: [py-pillow]
pkgconfig: [pkgconf, pkg-config] pkgconfig: [pkgconf, pkg-config]
scalapack: [netlib-scalapack] scalapack: [netlib-scalapack]
sycl: [hipsycl]
szip: [libszip, libaec] szip: [libszip, libaec]
tbb: [intel-tbb] tbb: [intel-tbb]
unwind: [libunwind] unwind: [libunwind]
......
...@@ -13,12 +13,18 @@ ...@@ -13,12 +13,18 @@
import select import select
import sys import sys
import traceback import traceback
import signal
from contextlib import contextmanager from contextlib import contextmanager
from six import string_types from six import string_types
from six import StringIO from six import StringIO
import llnl.util.tty as tty import llnl.util.tty as tty
try:
import termios
except ImportError:
termios = None
# Use this to strip escape sequences # Use this to strip escape sequences
_escape = re.compile(r'\x1b[^m]*m|\x1b\[?1034h') _escape = re.compile(r'\x1b[^m]*m|\x1b\[?1034h')
...@@ -31,12 +37,26 @@ ...@@ -31,12 +37,26 @@
control = re.compile('(\x11\n|\x13\n)') control = re.compile('(\x11\n|\x13\n)')
@contextmanager
def background_safe():
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
yield
signal.signal(signal.SIGTTOU, signal.SIG_DFL)
def _is_background_tty():
"""Return True iff this process is backgrounded and stdout is a tty"""
if sys.stdout.isatty():
return os.getpgrp() != os.tcgetpgrp(sys.stdout.fileno())
return False # not writing to tty, not background
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)
class keyboard_input(object): class _keyboard_input(object):
"""Context manager to disable line editing and echoing. """Context manager to disable line editing and echoing.
Use this with ``sys.stdin`` for keyboard input, e.g.:: Use this with ``sys.stdin`` for keyboard input, e.g.::
...@@ -81,32 +101,30 @@ def __enter__(self): ...@@ -81,32 +101,30 @@ def __enter__(self):
if not self.stream or not self.stream.isatty(): if not self.stream or not self.stream.isatty():
return return
try: # If this fails, self.old_cfg will remain None
# If this fails, self.old_cfg will remain None if termios and not _is_background_tty():
import termios
# save old termios settings # save old termios settings
fd = self.stream.fileno() old_cfg = termios.tcgetattr(self.stream)
self.old_cfg = termios.tcgetattr(fd)
# create new settings with canonical input and echo try:
# disabled, so keypresses are immediate & don't echo. # create new settings with canonical input and echo
self.new_cfg = termios.tcgetattr(fd) # disabled, so keypresses are immediate & don't echo.
self.new_cfg[3] &= ~termios.ICANON self.new_cfg = termios.tcgetattr(self.stream)
self.new_cfg[3] &= ~termios.ECHO self.new_cfg[3] &= ~termios.ICANON
self.new_cfg[3] &= ~termios.ECHO
# Apply new settings for terminal # Apply new settings for terminal
termios.tcsetattr(fd, termios.TCSADRAIN, self.new_cfg) termios.tcsetattr(self.stream, termios.TCSADRAIN, self.new_cfg)
self.old_cfg = old_cfg
except Exception: except Exception:
pass # some OS's do not support termios, so ignore pass # some OS's do not support termios, so ignore
def __exit__(self, exc_type, exception, traceback): def __exit__(self, exc_type, exception, traceback):
"""If termios was avaialble, restore old settings.""" """If termios was avaialble, restore old settings."""
if self.old_cfg: if self.old_cfg:
import termios with background_safe(): # change it back even if backgrounded now
termios.tcsetattr( termios.tcsetattr(self.stream, termios.TCSADRAIN, self.old_cfg)
self.stream.fileno(), termios.TCSADRAIN, self.old_cfg)
class Unbuffered(object): class Unbuffered(object):
...@@ -426,45 +444,63 @@ def _writer_daemon(self, stdin): ...@@ -426,45 +444,63 @@ def _writer_daemon(self, stdin):
istreams = [in_pipe, stdin] if stdin else [in_pipe] istreams = [in_pipe, stdin] if stdin else [in_pipe]
log_file = self.log_file log_file = self.log_file
def handle_write(force_echo):
# Handle output from the with block process.
# If we arrive here it means that in_pipe was
# ready for reading : it should never happen that
# line is false-ish
line = in_pipe.readline()
if not line:
return (True, force_echo) # break while loop
# find control characters and strip them.
controls = control.findall(line)
line = re.sub(control, '', line)
# Echo to stdout if requested or forced
if echo or force_echo:
try:
if termios:
conf = termios.tcgetattr(sys.stdout)
tostop = conf[3] & termios.TOSTOP
else:
tostop = True
except Exception:
tostop = True
if not (tostop and _is_background_tty()):
sys.stdout.write(line)
sys.stdout.flush()
# Stripped output to log file.
log_file.write(_strip(line))
log_file.flush()
if xon in controls:
force_echo = True
if xoff in controls:
force_echo = False
return (False, force_echo)
try: try:
with keyboard_input(stdin): with _keyboard_input(stdin):
while True: while True:
# No need to set any timeout for select.select # No need to set any timeout for select.select
# Wait until a key press or an event on in_pipe. # Wait until a key press or an event on in_pipe.
rlist, _, _ = select.select(istreams, [], []) rlist, _, _ = select.select(istreams, [], [])
# 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 stdin in rlist: # only read stdin if we're in the foreground
if stdin in rlist and not _is_background_tty():
if stdin.read(1) == 'v': if stdin.read(1) == 'v':
echo = not echo echo = not echo
# Handle output from the with block process.
if in_pipe in rlist: if in_pipe in rlist:
# If we arrive here it means that in_pipe was br, fe = handle_write(force_echo)
# ready for reading : it should never happen that force_echo = fe
# line is false-ish if br:
line = in_pipe.readline() break
if not line:
break # EOF
# find control characters and strip them.
controls = control.findall(line)
line = re.sub(control, '', line)
# Echo to stdout if requested or forced
if echo or force_echo:
sys.stdout.write(line)
sys.stdout.flush()
# Stripped output to log file.
log_file.write(_strip(line))
log_file.flush()
if xon in controls:
force_echo = True
if xoff in controls:
force_echo = False
except BaseException: except BaseException:
tty.error("Exception occurred in writer daemon!") tty.error("Exception occurred in writer daemon!")
traceback.print_exc() traceback.print_exc()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment