我正在使用Python脚本,正在寻找一种方法来将子进程的stdoutstderr重定向到日志记录模块。子过程是使用subprocess.call()方法创建的。

我面临的困难是,使用子过程我只能使用文件描述符重定向stdoutstderr。我没有找到其他方法,但是如果有其他方法,请告诉我!

为解决此问题,我编写了以下代码,该代码基本上创建了一个管道并使用线程从该管道读取并使用Python日志记录方法生成日志消息:

import subprocess
import logging
import os
import threading

class LoggerWrapper(threading.Thread):
    """
    Read text message from a pipe and redirect them
    to a logger (see python's logger module),
    the object itself is able to supply a file
    descriptor to be used for writing

    fdWrite ==> fdRead ==> pipeReader
    """

    def __init__(self, logger, level):
        """
        Setup the object with a logger and a loglevel
        and start the thread
        """

        # Initialize the superclass
        threading.Thread.__init__(self)

        # Make the thread a Daemon Thread (program will exit when only daemon
        # threads are alive)
        self.daemon = True

        # Set the logger object where messages will be redirected
        self.logger = logger

        # Set the log level
        self.level = level

        # Create the pipe and store read and write file descriptors
        self.fdRead, self.fdWrite = os.pipe()

        # Create a file-like wrapper around the read file descriptor
        # of the pipe, this has been done to simplify read operations
        self.pipeReader = os.fdopen(self.fdRead)

        # Start the thread
        self.start()
    # end __init__

    def fileno(self):
        """
        Return the write file descriptor of the pipe
        """
        return self.fdWrite
    # end fileno

    def run(self):
        """
        This is the method executed by the thread, it
        simply read from the pipe (using a file-like
        wrapper) and write the text to log.
        NB the trailing newline character of the string
           read from the pipe is removed
        """

        # Endless loop, the method will exit this loop only
        # when the pipe is close that is when a call to
        # self.pipeReader.readline() returns an empty string
        while True:

            # Read a line of text from the pipe
            messageFromPipe = self.pipeReader.readline()

            # If the line read is empty the pipe has been
            # closed, do a cleanup and exit
            # WARNING: I don't know if this method is correct,
            #          further study needed
            if len(messageFromPipe) == 0:
                self.pipeReader.close()
                os.close(self.fdRead)
                return
            # end if

            # Remove the trailing newline character frm the string
            # before sending it to the logger
            if messageFromPipe[-1] == os.linesep:
                messageToLog = messageFromPipe[:-1]
            else:
                messageToLog = messageFromPipe
            # end if

            # Send the text to the logger
            self._write(messageToLog)
        # end while

        print 'Redirection thread terminated'

    # end run

    def _write(self, message):
        """
        Utility method to send the message
        to the logger with the correct loglevel
        """
        self.logger.log(self.level, message)
    # end write

# end class LoggerWrapper

# # # # # # # # # # # # # #
# Code to test the class  #
# # # # # # # # # # # # # #
logging.basicConfig(filename='command.log',level=logging.INFO)
logWrap = LoggerWrapper( logging, logging.INFO)

subprocess.call(['cat', 'file_full_of_text.txt'], stdout = logWrap, stderr = logWrap)

print 'Script terminated'


对于日志记录子进程的输出,Google建议以类似于以下的方式直接将输出重定向到文件:

sobprocess.call( ['ls'] stdout = open( 'logfile.log', 'w') ) 


这不是我的选择,因为我需要使用日志记录模块的格式设置和loglevel工具。我还假定以写模式打开文件,但不允许两个不同的实体,也不是明智的选择。

我现在想看看您的评论和增强建议。我还想知道Python库中是否已有类似的对象,因为我什么也没找到来完成此任务的!

评论

您应该使用super()来调用超类方法。因此,不要编写thread.Thread .__ init __(self),而是编写super(LoggerWrapper,self).__ init __()。

当我尝试此操作时,在Ubuntu 10.04上的Python 2.6中,线程从未关闭。 self.pipeReader.readline()不断返回换行符。

#1 楼

好点子。我遇到了同样的问题,这帮助我解决了问题。但是,您执行清除的方法是错误的(就像您提到的那样)。基本上,您需要在将管道传递给子进程之后关闭管道的写入端。这样,当子进程退出并关闭其在管道的末端时,日志记录线程将得到一个SIGPIPE并返回您期望的零长度消息。

否则,主进程将永远保持管道的写端打开,从而导致readline无限期阻塞,这将导致您的线程与管道一样永远存在。一段时间后,这将成为一个主要问题,因为您将达到打开文件描述符数量的限制。

此外,该线程不应成为守护程序线程,因为这可能会丢失日志进程关闭期间的数据。如果按照说明正确清理,则所有线程将正确退出,从而无需将其标记为守护程序。

最后,可以使用while循环简化for循环。

实施所有这些更改将得到:

import logging
import threading
import os
import subprocess

logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)

class LogPipe(threading.Thread):

    def __init__(self, level):
        """Setup the object with a logger and a loglevel
        and start the thread
        """
        threading.Thread.__init__(self)
        self.daemon = False
        self.level = level
        self.fdRead, self.fdWrite = os.pipe()
        self.pipeReader = os.fdopen(self.fdRead)
        self.start()

    def fileno(self):
        """Return the write file descriptor of the pipe
        """
        return self.fdWrite

    def run(self):
        """Run the thread, logging everything.
        """
        for line in iter(self.pipeReader.readline, ''):
            logging.log(self.level, line.strip('\n'))

        self.pipeReader.close()

    def close(self):
        """Close the write end of the pipe.
        """
        os.close(self.fdWrite)

# For testing
if __name__ == "__main__":
    import sys

    logpipe = LogPipe(logging.INFO)
    with subprocess.Popen(['/bin/ls'], stdout=logpipe, stderr=logpipe) as s:
        logpipe.close()

    sys.exit()


我在几个地方使用了不同的名称,但除此之外,它是相同的想法,只是稍微更清洁,更坚固。

为子进程调用设置close_fds=True(实际上是默认设置)将无济于事,因为这会导致在调用exec之前在派生(子)进程中关闭文件描述符。但是我们需要在父进程中(即在fork之前)关闭文件描述符。

两个流仍然最终无法正确同步。我很确定原因是我们使用了两个单独的线程。我认为,如果我们仅在记录下使用一个线程,则该问题将得到解决。

问题是我们要处理两个不同的缓冲区(管道)。具有两个线程(现在我记得)通过在数据可用时写入数据来提供近似同步。它仍然是一个竞争条件,但是有两个“服务器”,因此通常没什么大不了的。由于只有一个线程,因此只有一个“服务器”,因此竞争状况以不同步的输出形式表现得非常糟糕。我想解决问题的唯一方法是改为扩展os.pipe(),但我不知道那是多么可行。

评论


\ $ \ begingroup \ $
sys.exit不调用sys.exit,该函数的值将被丢弃。 add()实际调用函数
\ $ \ endgroup \ $
– Caridorc
2015年9月8日在18:15



\ $ \ begingroup \ $
在启动线程之前,如何使用fdopen打开管道?为什么不阻止它?
\ $ \ endgroup \ $
– hakanc
2015年10月19日上午10:19

\ $ \ begingroup \ $
@Caridorc:固定
\ $ \ endgroup \ $
– deuberger
2015年10月29日在10:48

\ $ \ begingroup \ $
@hakanc:阻止不会影响打开。这是潜在的读写问题,这就是为什么这些部分在线程中完成的原因。同样,fdopen只是将已经打开的管道与File like对象包装在一起。据我所知,它实际上对文件没有任何作用。
\ $ \ endgroup \ $
– deuberger
15-10-29在10:58

\ $ \ begingroup \ $
建议:os.fdopen(self.fdRead)无法处理unicode输出。要支持unicode,请将其重写为os.fdopen(self.fdRead,encoding ='utf-8',errors ='ignore')。
\ $ \ endgroup \ $
– Hailinzeng
17-09-22在2:39

#2 楼

如果您不介意将STDOUTSTDERR记录在同一日志记录级别下,则可以执行以下操作:

import logging
import subprocess

logger = logging.getLogger(__name__)


def execute(system_command, **kwargs):
    """Execute a system command, passing STDOUT and STDERR to logger.

    Source: https://stackoverflow.com/a/4417735/2063031
    """
    logger.info("system_command: '%s'", system_command)
    popen = subprocess.Popen(
        shlex.split(system_command),
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True,
        **kwargs)
    for stdout_line in iter(popen.stdout.readline, ""):
        logger.debug(stdout_line.strip())
    popen.stdout.close()
    return_code = popen.wait()
    if return_code:
        raise subprocess.CalledProcessError(return_code, system_command)


这种方式您不必搞乱周围有螺纹。

评论


\ $ \ begingroup \ $
感谢您这样做的简单方法。但是,由于某种原因..即使在system_command完成后,文本文件仍处于繁忙状态。.是否有此原因?
\ $ \ endgroup \ $
–alpha_989
18年6月4日在2:30