-2

There's is no shortage of questions on SO about getting real-time stdout from a process with python. We also need it to be non-blocking, so:

while True:
  line = p.stdout.readline()
  if not line: break
  ...

...is not an option. Edit: Actually testing this solution now, it doesn't even work.

It's not even quite clear why that works and non-blocking versions using bufsize=1 don't. This is not a duplicate of Getting realtime output using subprocess, as none of the solutions suggested there seem to apply to a non-blocking version of the function. I've tested them. Not sure why bufsize=1 does not work.

There doesn't actually seem to be any solution now, because as the author of that answer cited above points out, it might have been a bug to begin with.

There are other questions about non-blocking versions of the the function too, but no question I've found asks for / receives answers to the problem of both non-blocking and forced unbuffering.

Here's a non-blocking func to run a command and poll for output:

import subprocess
from set_interval import set_interval

def call_command(cmd, callback=None):

    try:
        p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, encoding='utf-8', bufsize=1)

        def polling():

            for line in iter(p.stdout.readline, ''):

                if p.returncode != 0 or (not line and p.poll() is not None):
                    p.kill()
                    return False # stop interval
                else:
                    if callback is not None:
                        callback(line.strip())
                    else:
                        print(line.strip())
                    return True # continue interval

        set_interval(polling, 1)

        if p.returncode != 0:
            err = p.communicate()
            print(err[1].strip())

    except FileNotFoundError as e:
        print(f'{e} "{cmd[0]}"')

set_interval.py, referenced above:

import threading

def set_interval(func, sec):
    result = func()

    def func_wrapper():

        if result is True:
            set_interval(func, sec)
        
    t = threading.Timer(sec, func_wrapper)
    t.start()

The challenge we're facing is let's say the program generating output isn't flushing the output buffer:

output_unflushed.py:

from time import sleep
for i in range(10):
    print('i:', i)
    sleep(1)

Well if you do call_command('python output_unflushed.py'.split(' ')) you'll get no output for 10 seconds, then you'll get it all at once. This is because Python is just buffering all the stdout. As @radekholy24 pointed out we can fix that easily by modifying output_unflushed.py:

from time import sleep
import sys

for i in range(10):
    print('i:', i)
    sys.stdout.flush()
    sleep(1)

Unfortunately, we don't control the source of every process we want to run. Maybe we do, but not always/easily. For example, git clone seems to suffer from this same issue.

There are binaries that can be used to solve this (in theory, I tried the winpty solution and it didn't work for me), but that adds a lot of complexity:

This feels like major overkill, complicating my code, running it through another program, having to involve mingw dlls, etc. No thanks.

Also PYTHONUNBUFFERED=1 and similar process-specific solutions aren't what I'm interested in.

Is there any way to accomplish this with Python?

This must be possible in universally applicable way, since obviously the command prompts all manage it. Whether it's possible in Python (with non-blocking code) is a different story...

J.Todd
  • 517
  • 1
  • 8
  • 28
  • Does running the process (and yours) in unbuffered mode not work? – Andras Deak -- Слава Україні May 13 '22 at 05:24
  • @AndrasDeak--СлаваУкраїні Do you mean like set the environment variable `PYTHONUNBUFFERED=1`? That's case-by case support depending on the process, right? – J.Todd May 13 '22 at 05:30
  • Yes, that's what I meant. Partly because your example uses a python subprocess, so a python-only solution might still help you, and partly because I don't know how unbuffering the parent process affects child processes (such as git clone). – Andras Deak -- Слава Україні May 13 '22 at 05:37
  • 1
    @AndrasDeak--СлаваУкраїні I'll check out the parent process idea, not hopeful on that. As for the child process, yeah environment variables affecting just python or just git isnt really what we're after. I *know* this must be possible in universally applicable way, since obviously the command prompts all manage it. – J.Todd May 13 '22 at 05:40
  • I'm still not in front of a laptop, but if you have a blocking solution as per the linked question, you could just offload that to a thread (or equivalent functionality). – Andras Deak -- Слава Україні May 13 '22 at 06:31
  • @AndrasDeak--СлаваУкраїні That thought just entered my brain a few minutes ago.. I'll confirm the blocking answer I linked *does* work. I just assume it does, since it's the accepted answer and no negative comments. Will test in the AM. – J.Todd May 13 '22 at 06:34
  • I'm a bit confused about what's wrong with my question. Is there something I should improve? – J.Todd May 13 '22 at 06:47
  • @AndrasDeak--СлаваУкраїні In my initial testing, [that blocking solution](https://stackoverflow.com/questions/803265/getting-realtime-output-using-subprocess) accepted as the answer to the question I referenced before *does not work*. It was odd behavior to begin with, and may have been a bug, as mentioned by the answerer. – J.Todd May 13 '22 at 06:55
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/244711/discussion-on-question-by-j-todd-is-there-any-non-blocking-native-python-solut). – deceze May 13 '22 at 06:59

0 Answers0