Merge pull request #1938 from xonsh/sleepless

Sleepless
This commit is contained in:
Morten Enemark Lund 2016-11-08 06:44:29 +01:00 committed by GitHub
commit 3ce27a17fa
4 changed files with 87 additions and 51 deletions

View file

@ -48,6 +48,8 @@
```while True: sleep 1``. ```while True: sleep 1``.
* Fix for stdin redirects. * Fix for stdin redirects.
* Backgrounding works with ``$XONSH_STORE_STDOUT`` * Backgrounding works with ``$XONSH_STORE_STDOUT``
* ``PopenThread`` blocks its thread from finishing until command has completed
or process is suspended.
* Added a minimum time buffer time for command pipelines to check for * Added a minimum time buffer time for command pipelines to check for
if previous commands have executed successfully. This is helpful if previous commands have executed successfully. This is helpful
for pipelines where the last command takes a long time to start up, for pipelines where the last command takes a long time to start up,

View file

@ -426,7 +426,8 @@ class SubprocSpec:
self.captured_stderr = None self.captured_stderr = None
def __str__(self): def __str__(self):
s = self.cls.__name__ + '(' + str(self.cmd) + ', ' s = self.__class__.__name__ + '(' + str(self.cmd) + ', '
s += self.cls.__name__ + ', '
kws = [n + '=' + str(getattr(self, n)) for n in self.kwnames] kws = [n + '=' + str(getattr(self, n)) for n in self.kwnames]
s += ', '.join(kws) + ')' s += ', '.join(kws) + ')'
return s return s

View file

@ -115,7 +115,10 @@ class HistoryGC(threading.Thread):
file) tuples. file) tuples.
""" """
# pylint: disable=no-member # pylint: disable=no-member
xdd = builtins.__xonsh_env__.get('XONSH_DATA_DIR') env = getattr(builtins, '__xonsh_env__', None)
if env is None:
return []
xdd = env.get('XONSH_DATA_DIR')
xdd = expanduser_abs_path(xdd) xdd = expanduser_abs_path(xdd)
fs = [f for f in glob.iglob(os.path.join(xdd, 'xonsh-*.json'))] fs = [f for f in glob.iglob(os.path.join(xdd, 'xonsh-*.json'))]

View file

@ -82,24 +82,29 @@ class QueueReader:
""" """
self.fd = fd self.fd = fd
self.timeout = timeout self.timeout = timeout
self.sleepscale = 0
self.closed = False self.closed = False
self.queue = queue.Queue() self.queue = queue.Queue()
self.thread = None
def close(self): def close(self):
"""close the reader""" """close the reader"""
self.closed = True self.closed = True
def read_queue(self, timeout=None): def is_fully_read(self):
"""Reads a single chunk from the queue. This is non-blocking.""" """Returns whether or not the queue is fully read and the reader is
timeout = timeout or self.timeout closed.
"""
return (self.closed
and (self.thread is None or not self.thread.is_alive())
and self.queue.empty())
def read_queue(self):
"""Reads a single chunk from the queue. This is blocking if
the timeout is None and non-blocking otherwise.
"""
try: try:
self.sleepscale = 0 return self.queue.get(block=True, timeout=self.timeout)
return self.queue.get(block=timeout is not None,
timeout=timeout)
except queue.Empty: except queue.Empty:
self.sleepscale = min(self.sleepscale + 1, 3)
time.sleep(timeout * 10**self.sleepscale)
return b'' return b''
def read(self, size=-1): def read(self, size=-1):
@ -131,14 +136,26 @@ class QueueReader:
i += len(line) i += len(line)
return buf return buf
def _read_all_lines(self):
"""This reads all remaining lines in a blocking fashion."""
lines = []
while not self.is_fully_read():
chunk = self.read_queue()
lines.extend(chunk.splitlines(keepends=True))
return lines
def readlines(self, hint=-1): def readlines(self, hint=-1):
"""Reads lines from the file descriptor.""" """Reads lines from the file descriptor. This is blocking for negative
hints (i.e. read all the remaining lines) and non-blocking otherwise.
"""
if hint == -1:
return self._read_all_lines()
lines = [] lines = []
while len(lines) != hint: while len(lines) != hint:
line = self.readline(size=-1) chunk = self.read_queue()
if not line: if not chunk:
break break
lines.append(line) lines.extend(chunk.splitlines(keepends=True))
return lines return lines
def fileno(self): def fileno(self):
@ -150,6 +167,14 @@ class QueueReader:
"""Returns true, because this object is always readable.""" """Returns true, because this object is always readable."""
return True return True
def iterqueue(self):
"""Iterates through all remaining chunks in a blocking fashion."""
while not self.is_fully_read():
chunk = self.read_queue()
if not chunk:
continue
yield chunk
def populate_fd_queue(reader, fd, queue): def populate_fd_queue(reader, fd, queue):
"""Reads 1 kb of data from a file descriptor into a queue. """Reads 1 kb of data from a file descriptor into a queue.
@ -242,6 +267,8 @@ class BufferedFDParallelReader:
def _expand_console_buffer(cols, max_offset, expandsize, orig_posize, fd): def _expand_console_buffer(cols, max_offset, expandsize, orig_posize, fd):
# if we are getting close to the end of the console buffer, # if we are getting close to the end of the console buffer,
# expand it so that we can read from it successfully. # expand it so that we can read from it successfully.
if cols == 0:
return orig_posize[-1], max_offset, orig_posize
rows = ((max_offset + expandsize)//cols) + 1 rows = ((max_offset + expandsize)//cols) + 1
winutils.set_console_screen_buffer_size(cols, rows, fd=fd) winutils.set_console_screen_buffer_size(cols, rows, fd=fd)
orig_posize = orig_posize[:3] + (rows,) orig_posize = orig_posize[:3] + (rows,)
@ -251,9 +278,10 @@ def _expand_console_buffer(cols, max_offset, expandsize, orig_posize, fd):
def populate_console(reader, fd, buffer, chunksize, queue, expandsize=None): def populate_console(reader, fd, buffer, chunksize, queue, expandsize=None):
"""Reads bytes from the file descriptor and puts lines into the queue. """Reads bytes from the file descriptor and puts lines into the queue.
The reads happend in parallel, using xonsh.winutils.read_console_output_character(), The reads happend in parallel,
and is thus only available on windows. If the read fails for any reason, the reader is using xonsh.winutils.read_console_output_character(),
flagged as closed. and is thus only available on windows. If the read fails for any reason,
the reader is flagged as closed.
""" """
# OK, so this function is super annoying because Windows stores its # OK, so this function is super annoying because Windows stores its
# buffers as a 2D regular, dense array -- without trailing newlines. # buffers as a 2D regular, dense array -- without trailing newlines.
@ -316,7 +344,7 @@ def populate_console(reader, fd, buffer, chunksize, queue, expandsize=None):
if reader.closed: if reader.closed:
break break
else: else:
time.sleep(reader.timeout * 10**reader.sleepscale) time.sleep(reader.timeout)
continue continue
elif max_offset <= offset + expandsize: elif max_offset <= offset + expandsize:
ecb = _expand_console_buffer(cols, max_offset, expandsize, ecb = _expand_console_buffer(cols, max_offset, expandsize,
@ -348,7 +376,7 @@ def populate_console(reader, fd, buffer, chunksize, queue, expandsize=None):
buf = buf.rstrip() buf = buf.rstrip()
nread = len(buf) nread = len(buf)
if nread == 0: if nread == 0:
time.sleep(reader.timeout * 10**reader.sleepscale) time.sleep(reader.timeout)
continue continue
cur_x, cur_y = posize[0], posize[1] cur_x, cur_y = posize[0], posize[1]
cur_offset = (cols*cur_y) + cur_x cur_offset = (cols*cur_y) + cur_x
@ -364,7 +392,7 @@ def populate_console(reader, fd, buffer, chunksize, queue, expandsize=None):
for l in range(yshift)] for l in range(yshift)]
lines = [line for line in lines if line] lines = [line for line in lines if line]
if not lines: if not lines:
time.sleep(reader.timeout * 10**reader.sleepscale) time.sleep(reader.timeout)
continue continue
# put lines in the queue # put lines in the queue
nl = b'\n' nl = b'\n'
@ -469,9 +497,8 @@ class PopenThread(threading.Thread):
def __init__(self, *args, stdin=None, stdout=None, stderr=None, **kwargs): def __init__(self, *args, stdin=None, stdout=None, stderr=None, **kwargs):
super().__init__() super().__init__()
self.lock = threading.RLock() self.lock = threading.RLock()
self.stdout_fd = 1 if stdout is None else stdout.fileno()
self._set_pty_size()
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
# stdin setup
self.orig_stdin = stdin self.orig_stdin = stdin
if stdin is None: if stdin is None:
self.stdin_fd = 0 self.stdin_fd = 0
@ -482,6 +509,12 @@ class PopenThread(threading.Thread):
self.store_stdin = env.get('XONSH_STORE_STDIN') self.store_stdin = env.get('XONSH_STORE_STDIN')
self.in_alt_mode = False self.in_alt_mode = False
self.stdin_mode = None self.stdin_mode = None
# stdout setup
self.orig_stdout = stdout
self.stdout_fd = 1 if stdout is None else stdout.fileno()
self._set_pty_size()
# stderr setup
self.orig_stderr = stderr
# Set some signal handles, if we can. Must come before process # Set some signal handles, if we can. Must come before process
# is started to prevent deadlock on windows # is started to prevent deadlock on windows
self.proc = None # has to be here for closure for handles self.proc = None # has to be here for closure for handles
@ -561,33 +594,35 @@ class PopenThread(threading.Thread):
# loop over reads while process is running. # loop over reads while process is running.
cnt = 1 cnt = 1
while proc.poll() is None: while proc.poll() is None:
i = self._read_write(procout, stdout, sys.__stdout__) i = self._read_write(procout, stdout, sys.__stdout__)
j = self._read_write(procerr, stderr, sys.__stderr__) j = self._read_write(procerr, stderr, sys.__stderr__)
if self.suspended: if self.suspended:
break break
elif self.in_alt_mode: elif self.in_alt_mode:
# this is here for CPU performance reasons.
if i + j == 0: if i + j == 0:
cnt = min(cnt + 1, 1000) cnt = min(cnt + 1, 1000)
else: else:
cnt = 1 cnt = 1
time.sleep(self.timeout * cnt) procout.timeout = procerr.timeout = self.timeout * cnt
elif self.prevs_are_closed: if self.suspended:
break return
else: # close files to send EOF to non-blocking reader.
time.sleep(self.timeout) # capout & caperr seem to be needed only by Windows, while
# final closing read. # orig_stdout & orig_stderr are need by posix and Windows.
cntout = cnterr = 0 # Probably best to close them all. Also, order seems to matter here,
while cntout < 10 and cnterr < 10: # with orig_* needed to be closed before cap*
i = self._read_write(procout, stdout, sys.__stdout__) safe_fdclose(self.orig_stdout)
j = self._read_write(procerr, stderr, sys.__stderr__) safe_fdclose(self.orig_stderr)
cntout = 0 if i > 0 else cntout + 1 safe_fdclose(capout)
cnterr = 0 if j > 0 else cnterr + 1 safe_fdclose(caperr)
time.sleep(self.timeout * (10 - cntout)) # read in the remaining data in a blocking fashion.
while (procout is not None and not procout.is_fully_read()) or \
(procerr is not None and not procerr.is_fully_read()):
self._read_write(procout, stdout, sys.__stdout__)
self._read_write(procerr, stderr, sys.__stderr__)
# kill the process if it is still alive. Happens when piping. # kill the process if it is still alive. Happens when piping.
time.sleep(self.timeout) if proc.poll() is None:
if proc.poll() is None and not self.suspended:
time.sleep(self.timeout)
proc.terminate() proc.terminate()
def _wait_and_getattr(self, name): def _wait_and_getattr(self, name):
@ -692,7 +727,6 @@ class PopenThread(threading.Thread):
def _signal_int(self, signum, frame): def _signal_int(self, signum, frame):
"""Signal handler for SIGINT - Ctrl+C may have been pressed.""" """Signal handler for SIGINT - Ctrl+C may have been pressed."""
self.send_signal(signum) self.send_signal(signum)
time.sleep(self.timeout)
if self.proc.poll() is not None: if self.proc.poll() is not None:
self._restore_sigint(frame=frame) self._restore_sigint(frame=frame)
@ -794,9 +828,7 @@ class PopenThread(threading.Thread):
""" """
self._disable_cbreak_stdin() self._disable_cbreak_stdin()
rtn = self.proc.wait(timeout=timeout) rtn = self.proc.wait(timeout=timeout)
while self.is_alive(): self.join()
self.join(timeout=1e-7)
time.sleep(1e-7)
# need to replace the old signal handlers somewhere... # need to replace the old signal handlers somewhere...
if self.old_winch_handler is not None and on_main_thread(): if self.old_winch_handler is not None and on_main_thread():
signal.signal(signal.SIGWINCH, self.old_winch_handler) signal.signal(signal.SIGWINCH, self.old_winch_handler)
@ -1267,9 +1299,7 @@ class ProcProxyThread(threading.Thread):
def wait(self, timeout=None): def wait(self, timeout=None):
"""Waits for the process to finish and returns the return code.""" """Waits for the process to finish and returns the return code."""
while self.is_alive(): self.join()
self.join(timeout=1e-7)
time.sleep(1e-7)
return self.returncode return self.returncode
# The code below (_get_devnull, _get_handles, and _make_inheritable) comes # The code below (_get_devnull, _get_handles, and _make_inheritable) comes
@ -1558,6 +1588,8 @@ class CommandPipeline:
"stderr_redirect", "timestamps", "executed_cmd", 'input', "stderr_redirect", "timestamps", "executed_cmd", 'input',
'output', 'errors') 'output', 'errors')
nonblocking = (io.BytesIO, NonBlockingFDReader, ConsoleParallelReader)
def __init__(self, specs, procs, starttime=None, captured=False): def __init__(self, specs, procs, starttime=None, captured=False):
""" """
Parameters Parameters
@ -1635,8 +1667,7 @@ class CommandPipeline:
stdout = spec.captured_stdout stdout = spec.captured_stdout
if hasattr(stdout, 'buffer'): if hasattr(stdout, 'buffer'):
stdout = stdout.buffer stdout = stdout.buffer
if stdout is not None and \ if stdout is not None and not isinstance(stdout, self.nonblocking):
not isinstance(stdout, (io.BytesIO, NonBlockingFDReader, ConsoleParallelReader)):
stdout = NonBlockingFDReader(stdout.fileno(), timeout=timeout) stdout = NonBlockingFDReader(stdout.fileno(), timeout=timeout)
if not stdout or not safe_readable(stdout): if not stdout or not safe_readable(stdout):
# we get here if the process is not threadable or the # we get here if the process is not threadable or the
@ -1654,8 +1685,7 @@ class CommandPipeline:
stderr = spec.captured_stderr stderr = spec.captured_stderr
if hasattr(stderr, 'buffer'): if hasattr(stderr, 'buffer'):
stderr = stderr.buffer stderr = stderr.buffer
if stderr is not None and \ if stderr is not None and not isinstance(stderr, self.nonblocking):
not isinstance(stderr, (io.BytesIO, NonBlockingFDReader, ConsoleParallelReader)):
stderr = NonBlockingFDReader(stderr.fileno(), timeout=timeout) stderr = NonBlockingFDReader(stderr.fileno(), timeout=timeout)
# read from process while it is running # read from process while it is running
check_prev_done = len(self.procs) == 1 check_prev_done = len(self.procs) == 1