live output from subprocess command

后端 未结 16 1153
爱一瞬间的悲伤
爱一瞬间的悲伤 2020-11-22 08:16

I\'m using a python script as a driver for a hydrodynamics code. When it comes time to run the simulation, I use subprocess.Popen to run the code, collect the

相关标签:
16条回答
  • 2020-11-22 08:56

    If you're able to use third-party libraries, You might be able to use something like sarge (disclosure: I'm its maintainer). This library allows non-blocking access to output streams from subprocesses - it's layered over the subprocess module.

    0 讨论(0)
  • 2020-11-22 08:56

    A good but "heavyweight" solution is to use Twisted - see the bottom.

    If you're willing to live with only stdout something along those lines should work:

    import subprocess
    import sys
    popenobj = subprocess.Popen(["ls", "-Rl"], stdout=subprocess.PIPE)
    while not popenobj.poll():
       stdoutdata = popenobj.stdout.readline()
       if stdoutdata:
          sys.stdout.write(stdoutdata)
       else:
          break
    print "Return code", popenobj.returncode
    

    (If you use read() it tries to read the entire "file" which isn't useful, what we really could use here is something that reads all the data that's in the pipe right now)

    One might also try to approach this with threading, e.g.:

    import subprocess
    import sys
    import threading
    
    popenobj = subprocess.Popen("ls", stdout=subprocess.PIPE, shell=True)
    
    def stdoutprocess(o):
       while True:
          stdoutdata = o.stdout.readline()
          if stdoutdata:
             sys.stdout.write(stdoutdata)
          else:
             break
    
    t = threading.Thread(target=stdoutprocess, args=(popenobj,))
    t.start()
    popenobj.wait()
    t.join()
    print "Return code", popenobj.returncode
    

    Now we could potentially add stderr as well by having two threads.

    Note however the subprocess docs discourage using these files directly and recommends to use communicate() (mostly concerned with deadlocks which I think isn't an issue above) and the solutions are a little klunky so it really seems like the subprocess module isn't quite up to the job (also see: http://www.python.org/dev/peps/pep-3145/ ) and we need to look at something else.

    A more involved solution is to use Twisted as shown here: https://twistedmatrix.com/documents/11.1.0/core/howto/process.html

    The way you do this with Twisted is to create your process using reactor.spawnprocess() and providing a ProcessProtocol that then processes output asynchronously. The Twisted sample Python code is here: https://twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py

    0 讨论(0)
  • 2020-11-22 08:59

    You have two ways of doing this, either by creating an iterator from the read or readline functions and do:

    import subprocess
    import sys
    with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
        process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
        for c in iter(lambda: process.stdout.read(1), ''):  # replace '' with b'' for Python 3
            sys.stdout.write(c)
            f.write(c)
    

    or

    import subprocess
    import sys
    with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
        process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
        for line in iter(process.stdout.readline, ''):  # replace '' with b'' for Python 3
            sys.stdout.write(line)
            f.write(line)
    

    Or you can create a reader and a writer file. Pass the writer to the Popen and read from the reader

    import io
    import time
    import subprocess
    import sys
    
    filename = 'test.log'
    with io.open(filename, 'wb') as writer, io.open(filename, 'rb', 1) as reader:
        process = subprocess.Popen(command, stdout=writer)
        while process.poll() is None:
            sys.stdout.write(reader.read())
            time.sleep(0.5)
        # Read the remaining
        sys.stdout.write(reader.read())
    

    This way you will have the data written in the test.log as well as on the standard output.

    The only advantage of the file approach is that your code doesn't block. So you can do whatever you want in the meantime and read whenever you want from the reader in a non-blocking way. When you use PIPE, read and readline functions will block until either one character is written to the pipe or a line is written to the pipe respectively.

    0 讨论(0)
  • 2020-11-22 09:03

    In addition to all these answer, one simple approach could also be as follows:

    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    
    while process.stdout.readable():
        line = process.stdout.readline()
    
        if not line:
            break
    
        print(line.strip())
    

    Loop through the readable stream as long as it's readable and if it gets an empty result, stop.

    The key here is that readline() returns a line (with \n at the end) as long as there's an output and empty if it's really at the end.

    Hope this helps someone.

    0 讨论(0)
  • 2020-11-22 09:03

    Based on all the above I suggest a slightly modified version (python3):

    • while loop calling readline (The iter solution suggested seemed to block forever for me - Python 3, Windows 7)
    • structered so handling of read data does not need to be duplicated after poll returns not-None
    • stderr piped into stdout so both output outputs are read
    • Added code to get exit value of cmd.

    Code:

    import subprocess
    proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT, universal_newlines=True)
    while True:
        rd = proc.stdout.readline()
        print(rd, end='')  # and whatever you want to do...
        if not rd:  # EOF
            returncode = proc.poll()
            if returncode is not None:
                break
            time.sleep(0.1)  # cmd closed stdout, but not exited yet
    
    # You may want to check on ReturnCode here
    
    0 讨论(0)
  • Here is a class which I'm using in one of my projects. It redirects output of a subprocess to the log. At first I tried simply overwriting the write-method but that doesn't work as the subprocess will never call it (redirection happens on filedescriptor level). So I'm using my own pipe, similar to how it's done in the subprocess-module. This has the advantage of encapsulating all logging/printing logic in the adapter and you can simply pass instances of the logger to Popen: subprocess.Popen("/path/to/binary", stderr = LogAdapter("foo"))

    class LogAdapter(threading.Thread):
    
        def __init__(self, logname, level = logging.INFO):
            super().__init__()
            self.log = logging.getLogger(logname)
            self.readpipe, self.writepipe = os.pipe()
    
            logFunctions = {
                logging.DEBUG: self.log.debug,
                logging.INFO: self.log.info,
                logging.WARN: self.log.warn,
                logging.ERROR: self.log.warn,
            }
    
            try:
                self.logFunction = logFunctions[level]
            except KeyError:
                self.logFunction = self.log.info
    
        def fileno(self):
            #when fileno is called this indicates the subprocess is about to fork => start thread
            self.start()
            return self.writepipe
    
        def finished(self):
           """If the write-filedescriptor is not closed this thread will
           prevent the whole program from exiting. You can use this method
           to clean up after the subprocess has terminated."""
           os.close(self.writepipe)
    
        def run(self):
            inputFile = os.fdopen(self.readpipe)
    
            while True:
                line = inputFile.readline()
    
                if len(line) == 0:
                    #no new data was added
                    break
    
                self.logFunction(line.strip())
    

    If you don't need logging but simply want to use print() you can obviously remove large portions of the code and keep the class shorter. You could also expand it by an __enter__ and __exit__ method and call finished in __exit__ so that you could easily use it as context.

    0 讨论(0)
提交回复
热议问题