spack install terminal output handling in foreground/background (#15723)
Makes the following changes: * (Fixes #15620) tty configuration was failing when stdout was redirected. The implementation now creates a pseudo terminal for stdin and checks stdout properly, so redirections of stdin/out/err should be handled now. * Handles terminal configuration when the Spack process moves between the foreground and background (possibly multiple times) during a build. * Spack adjusts terminal settings to allow users to to enable/disable build process output to the terminal using a "v" toggle, abnormal exit cases (like CTRL-C) could leave the terminal in an unusable state. This is addressed here with a special-case handler which restores terminal settings. Significantly extend testing of process output logger: * New PseudoShell object for setting up a master and child process and configuring file descriptor inheritance between the two * Tests for "v" verbosity toggle making use of the added PseudoShell object * Added `uniq` function which takes a list of elements and replaces any consecutive sequence of duplicate elements with a single instance (e.g. "112211" -> "121") Co-authored-by: Adam J. Stewart <ajstewart426@gmail.com>
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import atexit
|
||||
import errno
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
@@ -25,6 +27,7 @@
|
||||
except ImportError:
|
||||
termios = None
|
||||
|
||||
|
||||
# Use this to strip escape sequences
|
||||
_escape = re.compile(r'\x1b[^m]*m|\x1b\[?1034h')
|
||||
|
||||
@@ -38,17 +41,22 @@
|
||||
|
||||
|
||||
@contextmanager
|
||||
def background_safe():
|
||||
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
||||
yield
|
||||
signal.signal(signal.SIGTTOU, signal.SIG_DFL)
|
||||
def ignore_signal(signum):
|
||||
"""Context manager to temporarily ignore a signal."""
|
||||
old_handler = signal.signal(signum, signal.SIG_IGN)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
signal.signal(signum, old_handler)
|
||||
|
||||
|
||||
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 _is_background_tty(stream):
|
||||
"""True if the stream is a tty and calling process is in the background.
|
||||
"""
|
||||
return (
|
||||
stream.isatty() and
|
||||
os.getpgrp() != os.tcgetpgrp(stream.fileno())
|
||||
)
|
||||
|
||||
|
||||
def _strip(line):
|
||||
@@ -56,27 +64,80 @@ def _strip(line):
|
||||
return _escape.sub('', line)
|
||||
|
||||
|
||||
class _keyboard_input(object):
|
||||
class keyboard_input(object):
|
||||
"""Context manager to disable line editing and echoing.
|
||||
|
||||
Use this with ``sys.stdin`` for keyboard input, e.g.::
|
||||
|
||||
with keyboard_input(sys.stdin):
|
||||
r, w, x = select.select([sys.stdin], [], [])
|
||||
# ... do something with keypresses ...
|
||||
with keyboard_input(sys.stdin) as kb:
|
||||
while True:
|
||||
kb.check_fg_bg()
|
||||
r, w, x = select.select([sys.stdin], [], [])
|
||||
# ... do something with keypresses ...
|
||||
|
||||
This disables canonical input so that keypresses are available on the
|
||||
stream immediately. Typically standard input allows line editing,
|
||||
which means keypresses won't be sent until the user hits return.
|
||||
The ``keyboard_input`` context manager disables canonical
|
||||
(line-based) input and echoing, so that keypresses are available on
|
||||
the stream immediately, and they are not printed to the
|
||||
terminal. Typically, standard input is line-buffered, which means
|
||||
keypresses won't be sent until the user hits return. In this mode, a
|
||||
user can hit, e.g., 'v', and it will be read on the other end of the
|
||||
pipe immediately but not printed.
|
||||
|
||||
It also disables echoing, so that keys pressed aren't printed to the
|
||||
terminal. So, the user can hit, e.g., 'v', and it's read on the
|
||||
other end of the pipe immediately but not printed.
|
||||
The handler takes care to ensure that terminal changes only take
|
||||
effect when the calling process is in the foreground. If the process
|
||||
is backgrounded, canonical mode and echo are re-enabled. They are
|
||||
disabled again when the calling process comes back to the foreground.
|
||||
|
||||
When the with block completes, prior TTY settings are restored.
|
||||
This context manager works through a single signal handler for
|
||||
``SIGTSTP``, along with a poolling routine called ``check_fg_bg()``.
|
||||
Here are the relevant states, transitions, and POSIX signals::
|
||||
|
||||
[Running] -------- Ctrl-Z sends SIGTSTP ------------.
|
||||
[ in FG ] <------- fg sends SIGCONT --------------. |
|
||||
^ | |
|
||||
| fg (no signal) | |
|
||||
| | v
|
||||
[Running] <------- bg sends SIGCONT ---------- [Stopped]
|
||||
[ in BG ] [ in BG ]
|
||||
|
||||
We handle all transitions exept for ``SIGTSTP`` generated by Ctrl-Z
|
||||
by periodically calling ``check_fg_bg()``. This routine notices if
|
||||
we are in the background with canonical mode or echo disabled, or if
|
||||
we are in the foreground without canonical disabled and echo enabled,
|
||||
and it fixes the terminal settings in response.
|
||||
|
||||
``check_fg_bg()`` works *except* for when the process is stopped with
|
||||
``SIGTSTP``. We cannot rely on a periodic timer in this case, as it
|
||||
may not rrun before the process stops. We therefore restore terminal
|
||||
settings in the ``SIGTSTP`` handler.
|
||||
|
||||
Additional notes:
|
||||
|
||||
* We mostly use polling here instead of a SIGARLM timer or a
|
||||
thread. This is to avoid the complexities of many interrupts, which
|
||||
seem to make system calls (like I/O) unreliable in older Python
|
||||
versions (2.6 and 2.7). See these issues for details:
|
||||
|
||||
1. https://www.python.org/dev/peps/pep-0475/
|
||||
2. https://bugs.python.org/issue8354
|
||||
|
||||
There are essentially too many ways for asynchronous signals to go
|
||||
wrong if we also have to support older Python versions, so we opt
|
||||
not to use them.
|
||||
|
||||
* ``SIGSTOP`` can stop a process (in the foreground or background),
|
||||
but it can't be caught. Because of this, we can't fix any terminal
|
||||
settings on ``SIGSTOP``, and the terminal will be left with
|
||||
``ICANON`` and ``ECHO`` disabled until it is resumes execution.
|
||||
|
||||
* Technically, a process *could* be sent ``SIGTSTP`` while running in
|
||||
the foreground, without the shell backgrounding that process. This
|
||||
doesn't happen in practice, and we assume that ``SIGTSTP`` always
|
||||
means that defaults should be restored.
|
||||
|
||||
* We rely on ``termios`` support. Without it, or if the stream isn't
|
||||
a TTY, ``keyboard_input`` has no effect.
|
||||
|
||||
Note: this depends on termios support. If termios isn't available,
|
||||
or if the stream isn't a TTY, this context manager has no effect.
|
||||
"""
|
||||
def __init__(self, stream):
|
||||
"""Create a context manager that will enable keyboard input on stream.
|
||||
@@ -89,42 +150,97 @@ def __init__(self, stream):
|
||||
"""
|
||||
self.stream = stream
|
||||
|
||||
def _is_background(self):
|
||||
"""True iff calling process is in the background."""
|
||||
return _is_background_tty(self.stream)
|
||||
|
||||
def _get_canon_echo_flags(self):
|
||||
"""Get current termios canonical and echo settings."""
|
||||
cfg = termios.tcgetattr(self.stream)
|
||||
return (
|
||||
bool(cfg[3] & termios.ICANON),
|
||||
bool(cfg[3] & termios.ECHO),
|
||||
)
|
||||
|
||||
def _enable_keyboard_input(self):
|
||||
"""Disable canonical input and echoing on ``self.stream``."""
|
||||
# "enable" input by disabling canonical mode and echo
|
||||
new_cfg = termios.tcgetattr(self.stream)
|
||||
new_cfg[3] &= ~termios.ICANON
|
||||
new_cfg[3] &= ~termios.ECHO
|
||||
|
||||
# Apply new settings for terminal
|
||||
with ignore_signal(signal.SIGTTOU):
|
||||
termios.tcsetattr(self.stream, termios.TCSANOW, new_cfg)
|
||||
|
||||
def _restore_default_terminal_settings(self):
|
||||
"""Restore the original input configuration on ``self.stream``."""
|
||||
# _restore_default_terminal_settings Can be called in foreground
|
||||
# or background. When called in the background, tcsetattr triggers
|
||||
# SIGTTOU, which we must ignore, or the process will be stopped.
|
||||
with ignore_signal(signal.SIGTTOU):
|
||||
termios.tcsetattr(self.stream, termios.TCSANOW, self.old_cfg)
|
||||
|
||||
def _tstp_handler(self, signum, frame):
|
||||
self._restore_default_terminal_settings()
|
||||
os.kill(os.getpid(), signal.SIGSTOP)
|
||||
|
||||
def check_fg_bg(self):
|
||||
# old_cfg is set up in __enter__ and indicates that we have
|
||||
# termios and a valid stream.
|
||||
if not self.old_cfg:
|
||||
return
|
||||
|
||||
# query terminal flags and fg/bg status
|
||||
flags = self._get_canon_echo_flags()
|
||||
bg = self._is_background()
|
||||
|
||||
# restore sanity if flags are amiss -- see diagram in class docs
|
||||
if not bg and any(flags): # fg, but input not enabled
|
||||
self._enable_keyboard_input()
|
||||
elif bg and not all(flags): # bg, but input enabled
|
||||
self._restore_default_terminal_settings()
|
||||
|
||||
def __enter__(self):
|
||||
"""Enable immediate keypress input on stream.
|
||||
"""Enable immediate keypress input, while this process is foreground.
|
||||
|
||||
If the stream is not a TTY or the system doesn't support termios,
|
||||
do nothing.
|
||||
"""
|
||||
self.old_cfg = None
|
||||
self.old_handlers = {}
|
||||
|
||||
# Ignore all this if the input stream is not a tty.
|
||||
if not self.stream or not self.stream.isatty():
|
||||
return
|
||||
return self
|
||||
|
||||
# If this fails, self.old_cfg will remain None
|
||||
if termios and not _is_background_tty():
|
||||
# save old termios settings
|
||||
old_cfg = termios.tcgetattr(self.stream)
|
||||
if termios:
|
||||
# save old termios settings to restore later
|
||||
self.old_cfg = termios.tcgetattr(self.stream)
|
||||
|
||||
try:
|
||||
# create new settings with canonical input and echo
|
||||
# disabled, so keypresses are immediate & don't echo.
|
||||
self.new_cfg = termios.tcgetattr(self.stream)
|
||||
self.new_cfg[3] &= ~termios.ICANON
|
||||
self.new_cfg[3] &= ~termios.ECHO
|
||||
# Install a signal handler to disable/enable keyboard input
|
||||
# when the process moves between foreground and background.
|
||||
self.old_handlers[signal.SIGTSTP] = signal.signal(
|
||||
signal.SIGTSTP, self._tstp_handler)
|
||||
|
||||
# Apply new settings for terminal
|
||||
termios.tcsetattr(self.stream, termios.TCSADRAIN, self.new_cfg)
|
||||
self.old_cfg = old_cfg
|
||||
# add an atexit handler to ensure the terminal is restored
|
||||
atexit.register(self._restore_default_terminal_settings)
|
||||
|
||||
except Exception:
|
||||
pass # some OS's do not support termios, so ignore
|
||||
# enable keyboard input initially (if foreground)
|
||||
if not self._is_background():
|
||||
self._enable_keyboard_input()
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exception, traceback):
|
||||
"""If termios was avaialble, restore old settings."""
|
||||
"""If termios was available, restore old settings."""
|
||||
if self.old_cfg:
|
||||
with background_safe(): # change it back even if backgrounded now
|
||||
termios.tcsetattr(self.stream, termios.TCSADRAIN, self.old_cfg)
|
||||
self._restore_default_terminal_settings()
|
||||
|
||||
# restore SIGSTP and SIGCONT handlers
|
||||
if self.old_handlers:
|
||||
for signum, old_handler in self.old_handlers.items():
|
||||
signal.signal(signum, old_handler)
|
||||
|
||||
|
||||
class Unbuffered(object):
|
||||
@@ -300,11 +416,11 @@ def __enter__(self):
|
||||
self._saved_debug = tty._debug
|
||||
|
||||
# OS-level pipe for redirecting output to logger
|
||||
self.read_fd, self.write_fd = os.pipe()
|
||||
read_fd, 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()
|
||||
self.parent_pipe, child_pipe = multiprocessing.Pipe()
|
||||
|
||||
# Sets a daemon that writes to file what it reads from a pipe
|
||||
try:
|
||||
@@ -315,10 +431,15 @@ def __enter__(self):
|
||||
input_stream = None # just don't forward input if this fails
|
||||
|
||||
self.process = multiprocessing.Process(
|
||||
target=self._writer_daemon, args=(input_stream,))
|
||||
target=_writer_daemon,
|
||||
args=(
|
||||
input_stream, read_fd, write_fd, self.echo, self.log_file,
|
||||
child_pipe
|
||||
)
|
||||
)
|
||||
self.process.daemon = True # must set before start()
|
||||
self.process.start()
|
||||
os.close(self.read_fd) # close in the parent process
|
||||
os.close(read_fd) # close in the parent process
|
||||
|
||||
finally:
|
||||
if input_stream:
|
||||
@@ -340,9 +461,9 @@ def __enter__(self):
|
||||
self._saved_stderr = os.dup(sys.stderr.fileno())
|
||||
|
||||
# redirect to the pipe we created above
|
||||
os.dup2(self.write_fd, sys.stdout.fileno())
|
||||
os.dup2(self.write_fd, sys.stderr.fileno())
|
||||
os.close(self.write_fd)
|
||||
os.dup2(write_fd, sys.stdout.fileno())
|
||||
os.dup2(write_fd, sys.stderr.fileno())
|
||||
os.close(write_fd)
|
||||
|
||||
else:
|
||||
# Handle I/O the Python way. This won't redirect lower-level
|
||||
@@ -355,7 +476,7 @@ def __enter__(self):
|
||||
self._saved_stderr = sys.stderr
|
||||
|
||||
# create a file object for the pipe; redirect to it.
|
||||
pipe_fd_out = os.fdopen(self.write_fd, 'w')
|
||||
pipe_fd_out = os.fdopen(write_fd, 'w')
|
||||
sys.stdout = pipe_fd_out
|
||||
sys.stderr = pipe_fd_out
|
||||
|
||||
@@ -394,14 +515,14 @@ def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
|
||||
# print log contents in parent if needed.
|
||||
if self.write_log_in_parent:
|
||||
string = self.parent.recv()
|
||||
string = self.parent_pipe.recv()
|
||||
self.file_like.write(string)
|
||||
|
||||
if self.close_log_in_parent:
|
||||
self.log_file.close()
|
||||
|
||||
# recover and store echo settings from the child before it dies
|
||||
self.echo = self.parent.recv()
|
||||
self.echo = self.parent_pipe.recv()
|
||||
|
||||
# join the daemon process. The daemon will quit automatically
|
||||
# when the write pipe is closed; we just wait for it here.
|
||||
@@ -426,90 +547,166 @@ def force_echo(self):
|
||||
# exactly before and after the text we want to echo.
|
||||
sys.stdout.write(xon)
|
||||
sys.stdout.flush()
|
||||
yield
|
||||
sys.stdout.write(xoff)
|
||||
sys.stdout.flush()
|
||||
|
||||
def _writer_daemon(self, stdin):
|
||||
"""Daemon that writes output to the log file and stdout."""
|
||||
# Use line buffering (3rd param = 1) since Python 3 has a bug
|
||||
# that prevents unbuffered text I/O.
|
||||
in_pipe = os.fdopen(self.read_fd, 'r', 1)
|
||||
os.close(self.write_fd)
|
||||
|
||||
echo = self.echo # initial echo setting, user-controllable
|
||||
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]
|
||||
|
||||
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:
|
||||
with _keyboard_input(stdin):
|
||||
while True:
|
||||
# No need to set any timeout for select.select
|
||||
# Wait until a key press or an event on in_pipe.
|
||||
rlist, _, _ = select.select(istreams, [], [])
|
||||
# Allow user to toggle echo with 'v' key.
|
||||
# Currently ignores other chars.
|
||||
# only read stdin if we're in the foreground
|
||||
if stdin in rlist and not _is_background_tty():
|
||||
if stdin.read(1) == 'v':
|
||||
echo = not echo
|
||||
|
||||
if in_pipe in rlist:
|
||||
br, fe = handle_write(force_echo)
|
||||
force_echo = fe
|
||||
if br:
|
||||
break
|
||||
|
||||
except BaseException:
|
||||
tty.error("Exception occurred in writer daemon!")
|
||||
traceback.print_exc()
|
||||
|
||||
yield
|
||||
finally:
|
||||
# send written data back to parent if we used a StringIO
|
||||
if self.write_log_in_parent:
|
||||
self.child.send(log_file.getvalue())
|
||||
log_file.close()
|
||||
sys.stdout.write(xoff)
|
||||
sys.stdout.flush()
|
||||
|
||||
# send echo value back to the parent so it can be preserved.
|
||||
self.child.send(echo)
|
||||
|
||||
def _writer_daemon(stdin, read_fd, write_fd, echo, log_file, control_pipe):
|
||||
"""Daemon used by ``log_output`` to write to a log file and to ``stdout``.
|
||||
|
||||
The daemon receives output from the parent process and writes it both
|
||||
to a log and, optionally, to ``stdout``. The relationship looks like
|
||||
this::
|
||||
|
||||
Terminal
|
||||
|
|
||||
| +-------------------------+
|
||||
| | Parent Process |
|
||||
+--------> | with log_output(): |
|
||||
| stdin | ... |
|
||||
| +-------------------------+
|
||||
| ^ | write_fd (parent's redirected stdout)
|
||||
| | control |
|
||||
| | pipe |
|
||||
| | v read_fd
|
||||
| +-------------------------+ stdout
|
||||
| | Writer daemon |------------>
|
||||
+--------> | read from read_fd | log_file
|
||||
stdin | write to out and log |------------>
|
||||
+-------------------------+
|
||||
|
||||
Within the ``log_output`` handler, the parent's output is redirected
|
||||
to a pipe from which the daemon reads. The daemon writes each line
|
||||
from the pipe to a log file and (optionally) to ``stdout``. The user
|
||||
can hit ``v`` to toggle output on ``stdout``.
|
||||
|
||||
In addition to the input and output file descriptors, the daemon
|
||||
interacts with the parent via ``control_pipe``. It reports whether
|
||||
``stdout`` was enabled or disabled when it finished and, if the
|
||||
``log_file`` is a ``StringIO`` object, then the daemon also sends the
|
||||
logged output back to the parent as a string, to be written to the
|
||||
``StringIO`` in the parent. This is mainly for testing.
|
||||
|
||||
Arguments:
|
||||
stdin (stream): input from the terminal
|
||||
read_fd (int): pipe for reading from parent's redirected stdout
|
||||
write_fd (int): parent's end of the pipe will write to (will be
|
||||
immediately closed by the writer daemon)
|
||||
echo (bool): initial echo setting -- controlled by user and
|
||||
preserved across multiple writer daemons
|
||||
log_file (file-like): file to log all output
|
||||
control_pipe (Pipe): multiprocessing pipe on which to send control
|
||||
information to the parent
|
||||
|
||||
"""
|
||||
# Use line buffering (3rd param = 1) since Python 3 has a bug
|
||||
# that prevents unbuffered text I/O.
|
||||
in_pipe = os.fdopen(read_fd, 'r', 1)
|
||||
os.close(write_fd)
|
||||
|
||||
# list of streams to select from
|
||||
istreams = [in_pipe, stdin] if stdin else [in_pipe]
|
||||
force_echo = False # parent can force echo for certain output
|
||||
|
||||
try:
|
||||
with keyboard_input(stdin) as kb:
|
||||
while True:
|
||||
# fix the terminal settings if we recently came to
|
||||
# the foreground
|
||||
kb.check_fg_bg()
|
||||
|
||||
# wait for input from any stream. use a coarse timeout to
|
||||
# allow other checks while we wait for input
|
||||
rlist, _, _ = _retry(select.select)(istreams, [], [], 1e-1)
|
||||
|
||||
# Allow user to toggle echo with 'v' key.
|
||||
# Currently ignores other chars.
|
||||
# only read stdin if we're in the foreground
|
||||
if stdin in rlist and not _is_background_tty(stdin):
|
||||
# it's possible to be backgrounded between the above
|
||||
# check and the read, so we ignore SIGTTIN here.
|
||||
with ignore_signal(signal.SIGTTIN):
|
||||
try:
|
||||
if stdin.read(1) == 'v':
|
||||
echo = not echo
|
||||
except IOError as e:
|
||||
# If SIGTTIN is ignored, the system gives EIO
|
||||
# to let the caller know the read failed b/c it
|
||||
# was in the bg. Ignore that too.
|
||||
if e.errno != errno.EIO:
|
||||
raise
|
||||
|
||||
if in_pipe in rlist:
|
||||
# Handle output from the calling process.
|
||||
line = _retry(in_pipe.readline)()
|
||||
if not line:
|
||||
break
|
||||
|
||||
# find control characters and strip them.
|
||||
controls = control.findall(line)
|
||||
line = control.sub('', 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:
|
||||
tty.error("Exception occurred in writer daemon!")
|
||||
traceback.print_exc()
|
||||
|
||||
finally:
|
||||
# send written data back to parent if we used a StringIO
|
||||
if isinstance(log_file, StringIO):
|
||||
control_pipe.send(log_file.getvalue())
|
||||
log_file.close()
|
||||
|
||||
# send echo value back to the parent so it can be preserved.
|
||||
control_pipe.send(echo)
|
||||
|
||||
|
||||
def _retry(function):
|
||||
"""Retry a call if errors indicating an interrupted system call occur.
|
||||
|
||||
Interrupted system calls return -1 and set ``errno`` to ``EINTR`` if
|
||||
certain flags are not set. Newer Pythons automatically retry them,
|
||||
but older Pythons do not, so we need to retry the calls.
|
||||
|
||||
This function converts a call like this:
|
||||
|
||||
syscall(args)
|
||||
|
||||
and makes it retry by wrapping the function like this:
|
||||
|
||||
_retry(syscall)(args)
|
||||
|
||||
This is a private function because EINTR is unfortunately raised in
|
||||
different ways from different functions, and we only handle the ones
|
||||
relevant for this file.
|
||||
|
||||
"""
|
||||
def wrapped(*args, **kwargs):
|
||||
while True:
|
||||
try:
|
||||
return function(*args, **kwargs)
|
||||
except IOError as e:
|
||||
if e.errno == errno.EINTR:
|
||||
continue
|
||||
raise
|
||||
except select.error as e:
|
||||
if e.args[0] == errno.EINTR:
|
||||
continue
|
||||
raise
|
||||
return wrapped
|
||||
|
||||
Reference in New Issue
Block a user