Украсьте \ делегируйте объект File, чтобы добавить функциональность

Я написал небольшой скрипт Python, который выполняет некоторые команды оболочки, используя модуль 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>

Мой вопрос в том, какой самый Pythonic способ сделать это? Я думал о трех вариантах:

  1. Унаследуйте файловый объект и переопределите метод write.
  2. Используйте класс Delegate, который реализует write.
  3. Каким-то образом подключитесь к самому PIPE.

ОБНОВЛЕНИЕ: эталонный тестовый сценарий

Я проверяю результаты с помощью этого скрипта, сохраненного как test.py:

#!/usr/bin/python
import sys

sys.stdout.write('OUT\n')
sys.stdout.flush()
sys.stderr.write('ERR\n')
sys.stderr.flush()

Любые идеи?


person Adam Matan    schedule 17.01.2011    source источник
comment
Я сдаюсь. Мне удалось обернуть (подклассировать) файл, чтобы записи добавляли метку времени и заголовок файла, но Popen работает с fds, и я не вижу, где происходит запись. Похоже, это просто os.execvp, а дескриптор stdout — это что-то волшебное. Итак, я создал подкласс Popen, но он игнорирует тот факт, что ему были переданы завернутые файлы и карта fd:title.   -  person TryPyPy    schedule 28.01.2011


Ответы (5)


1 и 2 являются разумными решениями, но переопределения write() будет недостаточно.

Проблема в том, что Popen нужны файловые дескрипторы для присоединения к процессу, поэтому файловые объекты Python не работают, они должны быть на уровне ОС. Чтобы решить эту проблему, у вас должен быть объект Python с дескриптором файла уровня ОС. Единственный способ, которым я могу решить эту проблему, - это использовать каналы, поэтому у вас есть дескриптор файла уровня ОС для записи. Но тогда вам нужен еще один поток, который сидит и опрашивает этот канал, чтобы прочитать что-то, чтобы он мог это зарегистрировать. (Так что это более строгая реализация 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])

Таким образом, этот класс запускает канал уровня ОС, к которому 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, у меня нет лучшей идеи).

person Lennart Regebro    schedule 29.01.2011
comment
+1: это похоже на то, что я собирался предложить, но я не смог заставить поток ведения журнала вести себя правильно. - person senderle; 31.01.2011
comment
это будет работать на винде?! у меня есть сомнения, потому что select на win только для сокетов, а не для файлов. - person Nas Banov; 02.02.2011
comment
Я не пробовал это в Windows, но если нет, то замените select на то, что вы делаете, чтобы проверить, есть ли что-то для чтения в канале под Windows. - person Lennart Regebro; 02.02.2011
comment
Это здорово, но происходит сбой (output.txt пуст) при запуске простых скриптов Python, которые что-то выводят на стандартный вывод, а что-то на стандартный вывод. Любая идея? - person Adam Matan; 03.02.2011
comment
@Adam Matan: Нет, как видите, это работает с 'ls', так что в принципе это правильно. Я не знаю, как работает ваш простой скрипт на Python, поэтому я не могу объяснить, почему он не работает, хотя сейчас я вижу ошибку: он не печатает остальную часть буфера при выходе, так что это может быть так. Поместите self.write(buf.decode()) последним в методе флешера. Возможно, сначала поместите sleep(1) в цикле. - person Lennart Regebro; 03.02.2011
comment
@Lennart Regebro - Спасибо. все еще не работает, см. сценарий в разделе обновления моего вопроса. - person Adam Matan; 03.02.2011
comment
+Bounty У меня пока не работает, но это самое близкое решение. Не могли бы вы взглянуть на сценарий в конце ответа? - person Adam Matan; 03.02.2011
comment
Я попытался изменить этот код, отправив сообщения журнала в отдельный регистратор, у которого есть обработчик, который записывает в файл, но иногда это приводит к усечению выходных файлов. См. мой вопрос здесь: stackoverflow.com/questions/58137972/ Буду признателен за любую информацию. - person berkelem; 27.09.2019

Я бы предложил вариант 3 с пакетом стандартной библиотеки logging. В этом случае я бы сказал, что остальные 2 были излишними.

person Chris    schedule 17.01.2011
comment
Как бы вы перенаправили PIPE? - person Adam Matan; 17.01.2011

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, но запись будет происходить на более низком уровне, пропуская метод записи.

person TryPyPy    schedule 28.01.2011

Это простое решение сработало для меня:

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')
person Apalala    schedule 03.02.2011
comment
Работает ли это со смешанными результатами ошибок\вывода? Это не удалось для моего эталонного сценария (см. обновление выше). - person Adam Matan; 03.02.2011
comment
@ Адам Матан Я попробовал это с run('ls /nonexistent', 'Does not exist'), чтобы проверить stderr, и это сработало. Вам нужно убедиться, что это stderr=sp.SDOUT в вызове Popen. Кстати, я изменяю код, чтобы удалить цикл s.poll(), потому что это, вероятно, ненужный, быстро перематываемый цикл. - person Apalala; 03.02.2011
comment
@ Адам Матан. Он также работает с вашим эталонным скриптом. Смотрите правки. Я использую Ubuntu 10.10 и Python 2.6.6). - person Apalala; 03.02.2011
comment
Обратите внимание, что этот эксперимент не позволяет добавлять временные метки к выходным данным, или это, вероятно, бесполезно. - person Apalala; 04.02.2011

При этом используются 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):
    # https://stackoverflow.com/a/7730201/190597
    '''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):
    # https://stackoverflow.com/a/7730201/190597
    '''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)
person unutbu    schedule 17.01.2011
comment
Это хорошее решение, но оно откладывает stdout\stderr до завершения процесса, а затем записывает их в файл журнала. Это означает, что временные метки указывают время регистрации, а не время событий, и теряет порядок событий (записываются все сообщения stdout, а затем записываются все сообщения stderr). - person Adam Matan; 17.01.2011