Search code examples
pythonsubprocesspopen

Popen does not give output immediately when available


I am trying to read from both stdout and stderr from a Popen and print them out. The command I am running with Popen is the following

#!/bin/bash

i=10
while (( i > 0 )); do
    sleep 1s
    echo heyo-$i
    i="$((i-1))"
done

echo 'to error' >&2

When I run this in the shell, I get one line of output and then a second break and then one line again, etc. However, I am unable to recreate this using python. I am starting two threads, one each to read from stdout and stderr, put the lines read into a Queue and another thread that takes items from this queue and prints them out. But with this, I see that all the output gets printed out at once, after the subprocess ends. I want the lines to be printed as and when they are echo'ed.

Here's my python code:

# The `randoms` is in the $PATH
proc = sp.Popen(['randoms'], stdout=sp.PIPE, stderr=sp.PIPE, bufsize=0)

q = Queue()

def stream_watcher(stream, name=None):
    """Take lines from the stream and put them in the q"""
    for line in stream:
        q.put((name, line))
    if not stream.closed:
        stream.close()

Thread(target=stream_watcher, args=(proc.stdout, 'out')).start()
Thread(target=stream_watcher, args=(proc.stderr, 'err')).start()

def displayer():
    """Take lines from the q and add them to the display"""
    while True:
        try:
            name, line = q.get(True, 1)
        except Empty:
            if proc.poll() is not None:
                break
        else:
            # Print line with the trailing newline character
            print(name.upper(), '->', line[:-1])
            q.task_done()

    print('-*- FINISHED -*-')

Thread(target=displayer).start()

Any ideas? What am I missing here?


Solution

  • Only stderr is unbuffered, not stdout. What you want cannot be done using the shell built-ins alone. The buffering behavior is defined in the stdio(3) C library, which applies line buffering only when the output is to a terminal. When the output is to a pipe, it is pipe-buffered, not line-buffered, and so the data is not transferred to the kernel and thence to the other end of the pipe until the pipe buffer fills.

    Moreover, the shell has no access to libc’s buffer-controlling functions, such as setbuf(3) and friends. The only possible solution within the shell is to launch your co-process on a pseudo-tty, and pty management is a complex topic. It is much easier to rewrite the equivalent shell script in a language that does grant access to low-level buffering features for output streams than to arrange to run something over a pty.

    However, if you call /bin/echo instead of the shell built-in echo, you may find it more to your liking. This works because now the whole line is flushed when the newly launched /bin/echo process terminates each time. This is hardly an efficient use of system resources, but may be an efficient use of your own.