Перенаправление результатов подпроцессов (stdout и stderr) в модуль регистрации

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

44 голоса | спросил DracoJem 6 TueEurope/Moscow2011-12-06T17:02:29+04:00Europe/Moscow12bEurope/MoscowTue, 06 Dec 2011 17:02:29 +0400 2011, 17:02:29

2 ответа


21

Отличная идея. У меня была такая же проблема, и это помогло мне решить ее. Однако ваш метод очистки - это неправильно (как вы уже упоминали). В принципе, вам нужно закрыть конец записи в трубах после передачи их в подпроцесс. Таким образом, когда дочерний процесс завершает работу и закрывает его конец, поток ведения журнала получает код 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(), но я не знаю, насколько это возможно.

ответил deuberger 26 +04002012-10-26T16:58:49+04:00312012bEurope/MoscowFri, 26 Oct 2012 16:58:49 +0400 2012, 16:58:49
0

Если вы не возражаете 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)

Таким образом, вам не нужно возиться с потоками.

ответил ostrokach 11 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowMon, 11 Sep 2017 17:51:49 +0300 2017, 17:51:49

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132