A non-blocking read on a subprocess.PIPE in Python

后端 未结 29 2654
醉酒成梦
醉酒成梦 2020-11-21 04:49

I\'m using the subprocess module to start a subprocess and connect to its output stream (standard output). I want to be able to execute non-blocking reads on its standard ou

相关标签:
29条回答
  • 2020-11-21 04:58

    Here is a simple solution based on threads which:

    • works on both Linux and Windows (not relying on select).
    • reads both stdout and stderr asynchronouly.
    • doesn't rely on active polling with arbitrary waiting time (CPU friendly).
    • doesn't use asyncio (which may conflict with other libraries).
    • runs until the child process terminates.

    printer.py

    import time
    import sys
    
    sys.stdout.write("Hello\n")
    sys.stdout.flush()
    time.sleep(1)
    sys.stdout.write("World!\n")
    sys.stdout.flush()
    time.sleep(1)
    sys.stderr.write("That's an error\n")
    sys.stderr.flush()
    time.sleep(2)
    sys.stdout.write("Actually, I'm fine\n")
    sys.stdout.flush()
    time.sleep(1)
    

    reader.py

    import queue
    import subprocess
    import sys
    import threading
    
    
    def enqueue_stream(stream, queue, type):
        for line in iter(stream.readline, b''):
            queue.put(str(type) + line.decode('utf-8'))
        stream.close()
    
    
    def enqueue_process(process, queue):
        process.wait()
        queue.put('x')
    
    
    p = subprocess.Popen('python printer.py', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    q = queue.Queue()
    to = threading.Thread(target=enqueue_stream, args=(p.stdout, q, 1))
    te = threading.Thread(target=enqueue_stream, args=(p.stderr, q, 2))
    tp = threading.Thread(target=enqueue_process, args=(p, q))
    te.start()
    to.start()
    tp.start()
    
    while True:
        line = q.get()
        if line[0] == 'x':
            break
        if line[0] == '2':  # stderr
            sys.stdout.write("\033[0;31m")  # ANSI red color
        sys.stdout.write(line[1:])
        if line[0] == '2':
            sys.stdout.write("\033[0m")  # reset ANSI code
        sys.stdout.flush()
    
    tp.join()
    to.join()
    te.join()
    
    0 讨论(0)
  • 2020-11-21 05:01

    Existing solutions did not work for me (details below). What finally worked was to implement readline using read(1) (based on this answer). The latter does not block:

    from subprocess import Popen, PIPE
    from threading import Thread
    def process_output(myprocess): #output-consuming thread
        nextline = None
        buf = ''
        while True:
            #--- extract line using read(1)
            out = myprocess.stdout.read(1)
            if out == '' and myprocess.poll() != None: break
            if out != '':
                buf += out
                if out == '\n':
                    nextline = buf
                    buf = ''
            if not nextline: continue
            line = nextline
            nextline = None
    
            #--- do whatever you want with line here
            print 'Line is:', line
        myprocess.stdout.close()
    
    myprocess = Popen('myprogram.exe', stdout=PIPE) #output-producing process
    p1 = Thread(target=process_output, args=(dcmpid,)) #output-consuming thread
    p1.daemon = True
    p1.start()
    
    #--- do whatever here and then kill process and thread if needed
    if myprocess.poll() == None: #kill process; will automatically stop thread
        myprocess.kill()
        myprocess.wait()
    if p1 and p1.is_alive(): #wait for thread to finish
        p1.join()
    

    Why existing solutions did not work:

    1. Solutions that require readline (including the Queue based ones) always block. It is difficult (impossible?) to kill the thread that executes readline. It only gets killed when the process that created it finishes, but not when the output-producing process is killed.
    2. Mixing low-level fcntl with high-level readline calls may not work properly as anonnn has pointed out.
    3. Using select.poll() is neat, but doesn't work on Windows according to python docs.
    4. Using third-party libraries seems overkill for this task and adds additional dependencies.
    0 讨论(0)
  • 2020-11-21 05:02

    Try the asyncproc module. For example:

    import os
    from asyncproc import Process
    myProc = Process("myprogram.app")
    
    while True:
        # check to see if process has ended
        poll = myProc.wait(os.WNOHANG)
        if poll != None:
            break
        # print any new output
        out = myProc.read()
        if out != "":
            print out
    

    The module takes care of all the threading as suggested by S.Lott.

    0 讨论(0)
  • 2020-11-21 05:03

    Working from J.F. Sebastian's answer, and several other sources, I've put together a simple subprocess manager. It provides the request non-blocking reading, as well as running several processes in parallel. It doesn't use any OS-specific call (that I'm aware) and thus should work anywhere.

    It's available from pypi, so just pip install shelljob. Refer to the project page for examples and full docs.

    0 讨论(0)
  • 2020-11-21 05:04

    EDIT: This implementation still blocks. Use J.F.Sebastian's answer instead.

    I tried the top answer, but the additional risk and maintenance of thread code was worrisome.

    Looking through the io module (and being limited to 2.6), I found BufferedReader. This is my threadless, non-blocking solution.

    import io
    from subprocess import PIPE, Popen
    
    p = Popen(['myprogram.exe'], stdout=PIPE)
    
    SLEEP_DELAY = 0.001
    
    # Create an io.BufferedReader on the file descriptor for stdout
    with io.open(p.stdout.fileno(), 'rb', closefd=False) as buffer:
      while p.poll() == None:
          time.sleep(SLEEP_DELAY)
          while '\n' in bufferedStdout.peek(bufferedStdout.buffer_size):
              line = buffer.readline()
              # do stuff with the line
    
      # Handle any remaining output after the process has ended
      while buffer.peek():
        line = buffer.readline()
        # do stuff with the line
    
    0 讨论(0)
  • 2020-11-21 05:05

    fcntl, select, asyncproc won't help in this case.

    A reliable way to read a stream without blocking regardless of operating system is to use Queue.get_nowait():

    import sys
    from subprocess import PIPE, Popen
    from threading  import Thread
    
    try:
        from queue import Queue, Empty
    except ImportError:
        from Queue import Queue, Empty  # python 2.x
    
    ON_POSIX = 'posix' in sys.builtin_module_names
    
    def enqueue_output(out, queue):
        for line in iter(out.readline, b''):
            queue.put(line)
        out.close()
    
    p = Popen(['myprogram.exe'], stdout=PIPE, bufsize=1, close_fds=ON_POSIX)
    q = Queue()
    t = Thread(target=enqueue_output, args=(p.stdout, q))
    t.daemon = True # thread dies with the program
    t.start()
    
    # ... do other things here
    
    # read line without blocking
    try:  line = q.get_nowait() # or q.get(timeout=.1)
    except Empty:
        print('no output yet')
    else: # got line
        # ... do something with line
    
    0 讨论(0)
提交回复
热议问题