Украсить\делегировать объект File для добавления функциональности
Я пишу небольшой Python script, который выполняет некоторые команды оболочки с помощью модуля subprocess
и вспомогательной функции:
import subprocess as sp
def run(command, description):
"""Runs a command in a formatted manner. Returns its return code."""
start=datetime.datetime.now()
sys.stderr.write('%-65s' % description)
s=sp.Popen(command, shell=True, stderr=sp.PIPE, stdout=sp.PIPE)
out,err=s.communicate()
end=datetime.datetime.now()
duration=end-start
status='Done' if s.returncode==0 else 'Failed'
print '%s (%d seconds)' % (status, duration.seconds)
Следующие строки читают стандартный вывод и ошибку:
s=sp.Popen(command, shell=True, stderr=sp.PIPE, stdout=sp.PIPE)
out,err=s.communicate()
Как вы можете видеть, stdout и stderr не используются. Предположим, что я хочу записать выходные сообщения и сообщения об ошибках в файл журнала в форматированном виде, например:
[STDOUT: 2011-01-17 14:53:55] <message>
[STDERR: 2011-01-17 14:53:56] <message>
Мой вопрос в том, какой самый питонический способ сделать это? Я подумал о трех вариантах:
- Наследовать объект файла и переопределить метод
write
.
- Используйте класс Delegate, который реализует
write
.
- Подключиться к самому
PIPE
.
ОБНОВЛЕНИЕ: контрольный тест script
Я проверяю результаты с помощью этого script, сохраненного как test.py
:
#!/usr/bin/python
import sys
sys.stdout.write('OUT\n')
sys.stdout.flush()
sys.stderr.write('ERR\n')
sys.stderr.flush()
Любые идеи?
Ответы
Ответ 1
1 и 2 являются разумными решениями, но переопределить write() будет недостаточно.
Проблема в том, что Popen нуждается в файлах для прикрепления к процессу, поэтому объекты файлов Python не работают, они должны быть уровнями ОС. Чтобы решить это, у вас должен быть объект Python с дескриптором файла os level. Единственный способ, которым я могу решить это, - использовать трубы, поэтому у вас есть дескриптор файла уровня os для записи. Но тогда вам понадобится другой поток, который сидит и опросыет, что труба для чтения вещей, чтобы он мог ее зарегистрировать. (Так что это более строго реализация 2, поскольку она делегирует журналирование).
Сказано и сделано:
import io
import logging
import os
import select
import subprocess
import time
import threading
LOG_FILENAME = 'output.log'
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)
class StreamLogger(io.IOBase):
def __init__(self, level):
self.level = level
self.pipe = os.pipe()
self.thread = threading.Thread(target=self._flusher)
self.thread.start()
def _flusher(self):
self._run = True
buf = b''
while self._run:
for fh in select.select([self.pipe[0]], [], [], 0)[0]:
buf += os.read(fh, 1024)
while b'\n' in buf:
data, buf = buf.split(b'\n', 1)
self.write(data.decode())
time.sleep(1)
self._run = None
def write(self, data):
return logging.log(self.level, data)
def fileno(self):
return self.pipe[1]
def close(self):
if self._run:
self._run = False
while self._run is not None:
time.sleep(1)
os.close(self.pipe[0])
os.close(self.pipe[1])
Таким образом, класс запускает канал уровня os, который Popen может присоединить к stdin/out/error для подпроцесса. Он также запускает поток, который выполняет опрос другого конца этого канала один раз в секунду для ведения журнала, который затем регистрируется с помощью модуля ведения журнала.
Возможно, этот класс должен реализовать больше вещей для полноты, но в любом случае он работает в этом случае.
Пример кода:
with StreamLogger(logging.INFO) as out:
with StreamLogger(logging.ERROR) as err:
subprocess.Popen("ls", stdout=out, stderr=err, shell=True)
output.log заканчивается так:
INFO:root:output.log
INFO:root:streamlogger.py
INFO:root:and
INFO:root:so
INFO:root:on
Протестировано с помощью Python 2.6, 2.7 и 3.1.
Я думаю, что любая реализация 1 и 3 должна будет использовать аналогичные методы. Это немного связано, но если вы не можете правильно составить журнал команд Popen, у меня нет лучшей идеи).
Ответ 2
Я бы предложил вариант 3, с logging стандартной библиотекой. В этом случае я бы сказал, что остальные 2 были излишними.
Ответ 3
Используется Adam Rosenfield make_async и read_async. В то время как мой первоначальный ответ использовал select.epoll
и, следовательно, был только Linux, теперь он использует select.select
, который должен работать под Unix или Windows.
Этот журнал выводит из подпроцесса на /tmp/test.log
, как он есть:
import logging
import subprocess
import shlex
import select
import fcntl
import os
import errno
def make_async(fd):
# /questions/5710/how-can-i-print-and-display-subprocess-stdout-and-stderr-output-without-distortion/45320#45320
'''add the O_NONBLOCK flag to a file descriptor'''
fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)
def read_async(fd):
# /questions/5710/how-can-i-print-and-display-subprocess-stdout-and-stderr-output-without-distortion/45320#45320
'''read some data from a file descriptor, ignoring EAGAIN errors'''
try:
return fd.read()
except IOError, e:
if e.errno != errno.EAGAIN:
raise e
else:
return ''
def log_process(proc,stdout_logger,stderr_logger):
loggers = { proc.stdout: stdout_logger, proc.stderr: stderr_logger }
def log_fds(fds):
for fd in fds:
out = read_async(fd)
if out.strip():
loggers[fd].info(out)
make_async(proc.stdout)
make_async(proc.stderr)
while True:
# Wait for data to become available
rlist, wlist, xlist = select.select([proc.stdout, proc.stderr], [], [])
log_fds(rlist)
if proc.poll() is not None:
# Corner case: check if more output was created
# between the last call to read_async and now
log_fds([proc.stdout, proc.stderr])
break
if __name__=='__main__':
formatter = logging.Formatter('[%(name)s: %(asctime)s] %(message)s')
handler = logging.FileHandler('/tmp/test.log','w')
handler.setFormatter(formatter)
stdout_logger=logging.getLogger('STDOUT')
stdout_logger.setLevel(logging.DEBUG)
stdout_logger.addHandler(handler)
stderr_logger=logging.getLogger('STDERR')
stderr_logger.setLevel(logging.DEBUG)
stderr_logger.addHandler(handler)
proc = subprocess.Popen(shlex.split('ls -laR /tmp'),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
log_process(proc,stdout_logger,stderr_logger)
Ответ 4
1 и 2 не будут работать. Здесь реализуется принцип:
import subprocess
import time
FileClass = open('tmptmp123123123.tmp', 'w').__class__
class WrappedFile(FileClass):
TIMETPL = "%Y-%m-%d %H:%M:%S"
TEMPLATE = "[%s: %s] "
def __init__(self, name, mode='r', buffering=None, title=None):
self.title = title or name
if buffering is None:
super(WrappedFile, self).__init__(name, mode)
else:
super(WrappedFile, self).__init__(name, mode, buffering)
def write(self, s):
stamp = time.strftime(self.TIMETPL)
if not s:
return
# Add a line with timestamp per line to be written
s = s.split('\n')
spre = self.TEMPLATE % (self.title, stamp)
s = "\n".join(["%s %s" % (spre, line) for line in s]) + "\n"
super(WrappedFile, self).write(s)
Причина, по которой он не работает, заключается в том, что Popen никогда не вызывает stdout.write. Обернутый файл будет работать нормально, когда мы назовем его метод записи, и даже будем писать, если он будет передан Popen, но запись будет выполняться на нижнем уровне, пропуская метод записи.
Ответ 5
Это простое решение для меня:
import sys
import datetime
import tempfile
import subprocess as sp
def run(command, description):
"""Runs a command in a formatted manner. Returns its return code."""
with tempfile.SpooledTemporaryFile(8*1024) as so:
print >> sys.stderr, '%-65s' % description
start=datetime.datetime.now()
retcode = sp.call(command, shell=True, stderr=sp.STDOUT, stdout=so)
end=datetime.datetime.now()
so.seek(0)
for line in so.readlines():
print >> sys.stderr,'logging this:', line.rstrip()
duration=end-start
status='Done' if retcode == 0 else 'Failed'
print >> sys.stderr, '%s (%d seconds)' % (status, duration.seconds)
REF_SCRIPT = r"""#!/usr/bin/python
import sys
sys.stdout.write('OUT\n')
sys.stdout.flush()
sys.stderr.write('ERR\n')
sys.stderr.flush()
"""
SCRIPT_NAME = 'refscript.py'
if __name__ == '__main__':
with open(SCRIPT_NAME, 'w') as script:
script.write(REF_SCRIPT)
run('python ' + SCRIPT_NAME, 'Reference script')