stdout
和stderr
重定向到日志记录模块。子过程是使用subprocess.call()
方法创建的。我面临的困难是,使用子过程我只能使用文件描述符重定向
stdout
和stderr
。我没有找到其他方法,但是如果有其他方法,请告诉我!为解决此问题,我编写了以下代码,该代码基本上创建了一个管道并使用线程从该管道读取并使用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库中是否已有类似的对象,因为我什么也没找到来完成此任务的!
#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 楼
如果您不介意将STDOUT
和STDERR
记录在同一日志记录级别下,则可以执行以下操作: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
评论
您应该使用super()来调用超类方法。因此,不要编写thread.Thread .__ init __(self),而是编写super(LoggerWrapper,self).__ init __()。当我尝试此操作时,在Ubuntu 10.04上的Python 2.6中,线程从未关闭。 self.pipeReader.readline()不断返回换行符。