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

后端 未结 29 2662
醉酒成梦
醉酒成梦 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:56

    My problem is a bit different as I wanted to collect both stdout and stderr from a running process, but ultimately the same since I wanted to render the output in a widget as its generated.

    I did not want to resort to many of the proposed workarounds using Queues or additional Threads as they should not be necessary to perform such a common task as running another script and collecting its output.

    After reading the proposed solutions and python docs I resolved my issue with the implementation below. Yes it only works for POSIX as I'm using the select function call.

    I agree that the docs are confusing and the implementation is awkward for such a common scripting task. I believe that older versions of python have different defaults for Popen and different explanations so that created a lot of confusion. This seems to work well for both Python 2.7.12 and 3.5.2.

    The key was to set bufsize=1 for line buffering and then universal_newlines=True to process as a text file instead of a binary which seems to become the default when setting bufsize=1.

    class workerThread(QThread):
       def __init__(self, cmd):
          QThread.__init__(self)
          self.cmd = cmd
          self.result = None           ## return code
          self.error = None            ## flag indicates an error
          self.errorstr = ""           ## info message about the error
    
       def __del__(self):
          self.wait()
          DEBUG("Thread removed")
    
       def run(self):
          cmd_list = self.cmd.split(" ")   
          try:
             cmd = subprocess.Popen(cmd_list, bufsize=1, stdin=None
                                            , universal_newlines=True
                                            , stderr=subprocess.PIPE
                                            , stdout=subprocess.PIPE)
          except OSError:
             self.error = 1
             self.errorstr = "Failed to execute " + self.cmd
             ERROR(self.errorstr)
          finally:
             VERBOSE("task started...")
          import select
          while True:
             try:
                r,w,x = select.select([cmd.stdout, cmd.stderr],[],[])
                if cmd.stderr in r:
                   line = cmd.stderr.readline()
                   if line != "":
                      line = line.strip()
                      self.emit(SIGNAL("update_error(QString)"), line)
                if cmd.stdout in r:
                   line = cmd.stdout.readline()
                   if line == "":
                      break
                   line = line.strip()
                   self.emit(SIGNAL("update_output(QString)"), line)
             except IOError:
                pass
          cmd.wait()
          self.result = cmd.returncode
          if self.result < 0:
             self.error = 1
             self.errorstr = "Task terminated by signal " + str(self.result)
             ERROR(self.errorstr)
             return
          if self.result:
             self.error = 1
             self.errorstr = "exit code " + str(self.result)
             ERROR(self.errorstr)
             return
          return
    

    ERROR, DEBUG and VERBOSE are simply macros that print output to the terminal.

    This solution is IMHO 99.99% effective as it still uses the blocking readline function, so we assume the sub process is nice and outputs complete lines.

    I welcome feedback to improve the solution as I am still new to Python.

提交回复
热议问题