diff options
-rw-r--r-- | doc/commonissues.rst | 65 | ||||
-rwxr-xr-x | tests/test_misc.py | 37 |
2 files changed, 102 insertions, 0 deletions
diff --git a/doc/commonissues.rst b/doc/commonissues.rst index 26d0e2b..85b8d00 100644 --- a/doc/commonissues.rst +++ b/doc/commonissues.rst @@ -9,6 +9,71 @@ handle back to a worker thread. The child is successfully spawned but you can't interact with it. The only way to make it work is to spawn and interact with the child all in the same thread. [Adam Kerrison] +Blocking Child +-------------- + +A child process that writes to its standard out will eventually block unless +you continue to call :meth:`~pexpect.spawn.expect` or some family of read +methods. Once the pipe buffer is filled, the child's call to write(2) +will block. If this child provides other services, such as a network server, +such services will be unavailable until the write(2) call returns. + +There are many solutions, the simplest is to simply expect ``pexpect.EOF``, +perhaps in a background thread, this ensures that all child process output +is read until the program exits, for example:: + + import pexpect, threading + + class DiscardOutput(threading.Thread): + def __init__(self, child): + self.child = child + threading.Thread.__init__(self) + + def run(self): + # discard all output, expecting to program exit + self.child.expect(pexpect.EOF) + + bash = pexpect.spawn('bash', echo=False) + bash.sendline('n=0; while [ $n -lt 10000 ]; do echo $n; ' + 'let n="$n + 1"; done; exit') + # in this example, we care only that we've reached #100, + bash.expect(re.compile('\s100\s')) + # after which, we don't care what happens; we begin discarding output + thread = DiscardOutput(bash) + thread.start() + thread.join() + +You may also use the "disown" facility of bash(1) to put the child process in +the background, then detatch it from the parent process:: + + from pexpect import spawn, EOF + + # first, start bash + jot = spawn('bash') + + # then start the process that will run for long time + jot.sendline('jot 100000 1') + + # Look for a marker (here, 220 surrounded by newlines) + jot.expect(re.compile('\s220\s')) + + # stop the jot(1) process (SIGSTOP) + jot.sendcontrol('z') + + # wait for bash to return to prompt + jot.expect(u'Stopped') + + # put jot(1) in background, + # disown process ownership, + # exit bash. + jot.sendline('bg; disown; exit') + + # wait for bash to exit + jot.expect(EOF) + + # the program jot(1) has continued and completed + # without requiring read from parent. + Timing issue with send() and sendline() --------------------------------------- diff --git a/tests/test_misc.py b/tests/test_misc.py index d5a707c..e439240 100755 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -24,6 +24,7 @@ import re import signal import time import tempfile +import threading import os import pexpect @@ -41,6 +42,42 @@ else: return s +class DiscardOutput(threading.Thread): + def __init__(self, child): + self.child = child + threading.Thread.__init__(self) + + def run(self): + # discard all output, expecting to program exit + self.child.expect(pexpect.EOF) + + +class TestCaseBlockingWrite(PexpectTestCase.PexpectTestCase): + BASH_SCRIPT=('n=0; while [ $n -lt 10000 ]; do echo $n; ' + 'let n="$n + 1"; done; echo END > {0}; exit') + + def test_discard_output_using_background_thread(self): + " Test that a background thread can discard output." + # this is slightly contrary to the recommendation in commonissues.rst + # of section "Threads" regarding whether a background thread can read + # from a file descriptor created in a main thread -- presumably only + # an issue in RHEL 8. This test case matches the first recommendation + # of section, "Blocking Child" of the same document. + bash = pexpect.spawn('bash', echo=False) + result_file = tempfile.mktemp() + try: + bash.sendline(self.BASH_SCRIPT.format(result_file)) + bash.expect(re.compile('\s100\s'), timeout=2) + thread = DiscardOutput(bash) + thread.start() + thread.join() + assert os.path.exists(result_file) + assert open(result_file).read().strip() == 'END' + finally: + if os.path.exists(result_file): + os.unlink(result_file) + + class TestCaseMisc(PexpectTestCase.PexpectTestCase): def test_isatty(self): |