Subprocess.Popen: cloning stdout and stderr both to terminal and variables Subprocess.Popen: cloning stdout and stderr both to terminal and variables python-3.x python-3.x

Subprocess.Popen: cloning stdout and stderr both to terminal and variables


To capture and display at the same time both stdout and stderr from a child process line by line in a single thread, you could use asynchronous I/O:

#!/usr/bin/env python3import asyncioimport osimport sysfrom asyncio.subprocess import PIPE@asyncio.coroutinedef read_stream_and_display(stream, display):    """Read from stream line by line until EOF, display, and capture the lines.    """    output = []    while True:        line = yield from stream.readline()        if not line:            break        output.append(line)        display(line) # assume it doesn't block    return b''.join(output)@asyncio.coroutinedef read_and_display(*cmd):    """Capture cmd's stdout, stderr while displaying them as they arrive    (line by line).    """    # start process    process = yield from asyncio.create_subprocess_exec(*cmd,            stdout=PIPE, stderr=PIPE)    # read child's stdout/stderr concurrently (capture and display)    try:        stdout, stderr = yield from asyncio.gather(            read_stream_and_display(process.stdout, sys.stdout.buffer.write),            read_stream_and_display(process.stderr, sys.stderr.buffer.write))    except Exception:        process.kill()        raise    finally:        # wait for the process to exit        rc = yield from process.wait()    return rc, stdout, stderr# run the event loopif os.name == 'nt':    loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows    asyncio.set_event_loop(loop)else:    loop = asyncio.get_event_loop()rc, *output = loop.run_until_complete(read_and_display(*cmd))loop.close()


You could spawn threads to read the stdout and stderr pipes, write to a common queue, and append to lists. Then use a third thread to print items from the queue.

import timeimport Queueimport sysimport threadingimport subprocessPIPE = subprocess.PIPEdef read_output(pipe, funcs):    for line in iter(pipe.readline, ''):        for func in funcs:            func(line)            # time.sleep(1)    pipe.close()def write_output(get):    for line in iter(get, None):        sys.stdout.write(line)process = subprocess.Popen(    ['random_print.py'], stdout=PIPE, stderr=PIPE, close_fds=True, bufsize=1)q = Queue.Queue()out, err = [], []tout = threading.Thread(    target=read_output, args=(process.stdout, [q.put, out.append]))terr = threading.Thread(    target=read_output, args=(process.stderr, [q.put, err.append]))twrite = threading.Thread(target=write_output, args=(q.get,))for t in (tout, terr, twrite):    t.daemon = True    t.start()process.wait()for t in (tout, terr):    t.join()q.put(None)print(out)print(err)

The reason for using the third thread -- instead of letting the first two threads both print directly to the terminal -- is to prevent both print statements from occurring concurrently, which can result in sometimes garbled text.


The above calls random_print.py, which prints to stdout and stderr at random:

import sysimport timeimport randomfor i in range(50):    f = random.choice([sys.stdout,sys.stderr])    f.write(str(i)+'\n')    f.flush()    time.sleep(0.1)

This solution borrows code and ideas from J. F. Sebastian, here.


Here is an alternative solution for Unix-like systems, using select.select:

import collectionsimport selectimport fcntlimport osimport timeimport Queueimport sysimport threadingimport subprocessPIPE = subprocess.PIPEdef make_async(fd):    # https://stackoverflow.com/a/7730201/190597    '''add the O_NONBLOCK flag to a file descriptor'''    fcntl.fcntl(        fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)def read_async(fd):    # https://stackoverflow.com/a/7730201/190597    '''read some data from a file descriptor, ignoring EAGAIN errors'''    # time.sleep(1)    try:        return fd.read()    except IOError, e:        if e.errno != errno.EAGAIN:            raise e        else:            return ''def write_output(fds, outmap):    for fd in fds:        line = read_async(fd)        sys.stdout.write(line)        outmap[fd.fileno()].append(line)process = subprocess.Popen(    ['random_print.py'], stdout=PIPE, stderr=PIPE, close_fds=True)make_async(process.stdout)make_async(process.stderr)outmap = collections.defaultdict(list)while True:    rlist, wlist, xlist = select.select([process.stdout, process.stderr], [], [])    write_output(rlist, outmap)    if process.poll() is not None:        write_output([process.stdout, process.stderr], outmap)        breakfileno = {'stdout': process.stdout.fileno(),          'stderr': process.stderr.fileno()}print(outmap[fileno['stdout']])print(outmap[fileno['stderr']])

This solution uses code and ideas from Adam Rosenfield's post, here.