Merging a Python script's subprocess' stdout and stderr while keeping them distinguishable

前端 未结 6 1727
轮回少年
轮回少年 2020-12-04 14:44

I would like to direct a python script\'s subprocess\' stdout and stdin into the same file. What I don\'t know is how to make the lines from the two sources distinguishable?

相关标签:
6条回答
  • 2020-12-04 15:05

    If you want to interleave to get roughly the same order that you would if you ran the process interactively then you need to do what the shell does and poll stdin/stdout and write in the order that they poll.

    Here's some code that does something along the lines of what you want - in this case sending the stdout/stderr to a logger info/error streams.

    tsk = subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    
    poll = select.poll()
    poll.register(tsk.stdout,select.POLLIN | select.POLLHUP)
    poll.register(tsk.stderr,select.POLLIN | select.POLLHUP)
    pollc = 2
    
    events = poll.poll()
    while pollc > 0 and len(events) > 0:
      for event in events:
        (rfd,event) = event
        if event & select.POLLIN:
          if rfd == tsk.stdout.fileno():
            line = tsk.stdout.readline()
            if len(line) > 0:
              logger.info(line[:-1])
          if rfd == tsk.stderr.fileno():
            line = tsk.stderr.readline()
            if len(line) > 0:
              logger.error(line[:-1])
        if event & select.POLLHUP:
          poll.unregister(rfd)
          pollc = pollc - 1
        if pollc > 0: events = poll.poll()
    tsk.wait()
    
    0 讨论(0)
  • 2020-12-04 15:09

    At the moment all other answers don't handle buffering on the child subprocess' side if the subprocess is not a Python script that accepts -u flag. See "Q: Why not just use a pipe (popen())?" in the pexpect documentation.

    To simulate -u flag for some of C stdio-based (FILE*) programs you could try stdbuf.

    If you ignore this then your output won't be properly interleaved and might look like:

    stderr
    stderr
    ...large block of stdout including parts that are printed before stderr...
    

    You could try it with the following client program, notice the difference with/without -u flag (['stdbuf', '-o', 'L', 'child_program'] also fixes the output):

    #!/usr/bin/env python
    from __future__ import print_function
    import random
    import sys
    import time
    from datetime import datetime
    
    def tprint(msg, file=sys.stdout):
        time.sleep(.1*random.random())
        print("%s %s" % (datetime.utcnow().strftime('%S.%f'), msg), file=file)
    
    tprint("stdout1 before stderr")
    tprint("stdout2 before stderr")
    for x in range(5):
        tprint('stderr%d' % x, file=sys.stderr)
    tprint("stdout3 after stderr")
    

    On Linux you could use pty to get the same behavior as when the subprocess runs interactively e.g., here's a modified @T.Rojan's answer:

    import logging, os, select, subprocess, sys, pty
    
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    
    master_fd, slave_fd = pty.openpty()
    p = subprocess.Popen(args,stdout=slave_fd, stderr=subprocess.PIPE, close_fds=True)
    with os.fdopen(master_fd) as stdout:
        poll = select.poll()
        poll.register(stdout, select.POLLIN)
        poll.register(p.stderr,select.POLLIN | select.POLLHUP)
    
        def cleanup(_done=[]):
            if _done: return
            _done.append(1)
            poll.unregister(p.stderr)
            p.stderr.close()
            poll.unregister(stdout)
            assert p.poll() is not None
    
        read_write = {stdout.fileno(): (stdout.readline, logger.info),
                      p.stderr.fileno(): (p.stderr.readline, logger.error)}
        while True:
            events = poll.poll(40) # poll with a small timeout to avoid both
                                   # blocking forever and a busy loop
            if not events and p.poll() is not None:
                # no IO events and the subprocess exited
                cleanup()
                break
    
            for fd, event in events:
                if event & select.POLLIN: # there is something to read
                    read, write = read_write[fd]
                    line = read()
                    if line:
                        write(line.rstrip())
                elif event & select.POLLHUP: # free resources if stderr hung up
                    cleanup()
                else: # something unexpected happened
                    assert 0
    sys.exit(p.wait()) # return child's exit code
    

    It assumes that stderr is always unbuffered/line-buffered and stdout is line-buffered in an interactive mode. Only full lines are read. The program might block if there are non-terminated lines in the output.

    0 讨论(0)
  • 2020-12-04 15:10
    tsk = subprocess.Popen(args,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
    

    subprocess.STDOUT is a special flag that tells subprocess to route all stderr output to stdout, thus combining your two streams.

    btw, select doesn't have a poll() in windows. subprocess only uses the file handle number, and doesn't call your file output object's write method.

    to capture the output, do something like:

    logfile = open(logfilename, 'w')
    
    while tsk.poll() is None:
        line = tsk.stdout.readline()
        logfile.write(line)
    
    0 讨论(0)
  • 2020-12-04 15:17

    I found myself having to tackle this problem recently, and it took a while to get something I felt worked correctly in most cases, so here it is! (It also has the nice side effect of processing the output via a python logger, which I've noticed is another common question here on Stackoverflow).

    Here is the code:

    import sys
    import logging
    import subprocess
    from threading import Thread
    
    logging.basicConfig(stream=sys.stdout,level=logging.INFO)
    logging.addLevelName(logging.INFO+2,'STDERR')
    logging.addLevelName(logging.INFO+1,'STDOUT')
    logger = logging.getLogger('root')
    
    pobj = subprocess.Popen(['python','-c','print 42;bargle'], 
        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    
    def logstream(stream,loggercb):
        while True:
            out = stream.readline()
            if out:
                loggercb(out.rstrip())       
            else:
                break
    
    stdout_thread = Thread(target=logstream,
        args=(pobj.stdout,lambda s: logger.log(logging.INFO+1,s)))
    
    stderr_thread = Thread(target=logstream,
        args=(pobj.stderr,lambda s: logger.log(logging.INFO+2,s)))
    
    stdout_thread.start()
    stderr_thread.start()
    
    while stdout_thread.isAlive() and stderr_thread.isAlive():
         pass
    

    Here is the output:

    STDOUT:root:42
    STDERR:root:Traceback (most recent call last):
    STDERR:root:  File "<string>", line 1, in <module>
    STDERR:root:NameError: name 'bargle' is not defined
    

    You can replace the subprocess call to do whatever you want, I just chose running python with a command that I knew would print to both stdout and stderr. The key bit is reading stderr and stdout each in a separate thread. Otherwise you may be blocking on reading one while there is data ready to be read on the other.

    0 讨论(0)
  • 2020-12-04 15:17

    I suggest you write your own handlers, something like (not tested, I hope you catch the idea):

    class my_buffer(object):
        def __init__(self, fileobject, prefix):
            self._fileobject = fileobject
            self.prefix = prefix
        def write(self, text):
            return self._fileobject.write('%s %s' % (self.prefix, text))
        # delegate other methods to fileobject if necessary
    
    log_file = open('log.log', 'w')
    my_out = my_buffer(log_file, 'OK:')
    my_err = my_buffer(log_file, '!!!ERROR:')
    p = subprocess.Popen(command, stdout=my_out, stderr=my_err, shell=True)
    
    0 讨论(0)
  • 2020-12-04 15:20

    You may write the stdout/err to a file after the command execution. In the example below I use pickling so I am sure I will be able to read without any particular parsing to differentiate between the stdout/err and at some point I could dumo the exitcode and the command itself.

    import subprocess
    import cPickle
    
    command = 'ls -altrh'
    outfile = 'log.errout'
    pipe = subprocess.Popen(command, stdout = subprocess.PIPE,
                            stderr = subprocess.PIPE, shell = True)
    stdout, stderr = pipe.communicate()
    
    f = open(outfile, 'w')
    cPickle.dump({'out': stdout, 'err': stderr},f)
    f.close()
    
    0 讨论(0)
提交回复
热议问题