Python: поймать исключение из подпроцесса при повторении через стандартный вывод

Я пытаюсь получить исключение из подпроцесса. Я могу получить его, если использую .communicate, но я хотел бы избежать его использования, поскольку я передаю вывод из подпроцесса по мере его возникновения и не хочу ждать, пока весь подпроцесс завершится. Также предположим, что весь подпроцесс может занять очень много времени. Мне было интересно, как я могу поймать исключение, возникающее при потоковой передаче стандартного вывода из подпроцесса.

Рассмотрим приведенный ниже пример, поэтому я хотел бы, чтобы версия №1 работала, версия №2 вроде бы работает, но не хочу, чтобы это было так.

В main.py

import subprocess


class ExtProcess():
    def __init__(self, *args):
        self.proc = subprocess.Popen(['python', *args], stdout=subprocess.PIPE)

    def __iter__(self):
        return self

    def __next__(self):
        while True:
            line = self.proc.stdout.readline()
            if self.proc.returncode:
                raise Exception("error here")
            if not line:
                raise StopIteration
            return line


def run():
    ## version #1
    reader = ExtProcess("sample_extproc.py")
    for d in reader:
        print(f"got: {d}")

    ## version #2
    # proc = subprocess.Popen(['python', "sample_extproc.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    # output, error = proc.communicate()
    # print("got:", output)
    # if proc.returncode:
    #     raise Exception(error)

def main():
    try:
        print("start...")
        run()
        print("complete...")
    except Exception as e:
        print(f"Package midstream error here: {str(e)}")
    finally:
        print("clean up...")


if __name__ == "__main__":
    main()

В sample_extproc.py

for x in range(10):
    print(x)
    if x == 3:
        raise RuntimeError("catch me")

Я хотел бы получить вывод, как показано ниже, из версии № 1:

start...
got: b'0\r\n'
got: b'1\r\n'
got: b'2\r\n'
got: b'3\r\n'
Package midstream error here: b'Traceback (most recent call last):\r\n  File "sample_extproc.py", line 4, in <module>\r\n    raise RuntimeError("catch me")\r\nRuntimeError: catch me\r\n'
clean up...

По сути, он перебирает стандартный вывод из подпроцесса, затем печатает исключение, когда оно возникает, а затем продолжает выполнять очистку.


person user1179317    schedule 14.10.2020    source источник
comment
Программы не возвращают статус выхода, пока не выйдут. Ваш подпроцесс завершается, когда он выдает исключение?   -  person Charles Duffy    schedule 14.10.2020
comment
...поэтому очень мало смысла пытаться проверить returncode, пока вы все еще читаете данные. Конечно, процесс может закрыть свой stdout перед выходом или несколько десятков байтов все еще будут находиться в FIFO во время выхода, но очень необычно, чтобы это занимало больше миллисекунд. , и даже тогда родительский процесс не будет знать статус выхода, пока он не вызовет системный вызов wait() для его извлечения (при получении PID завершенного процесса из записи зомби, которую он оставляет в таблице процессов на время между вызовом выхода и его родительский процесс, читающий этот статус).   -  person Charles Duffy    schedule 14.10.2020
comment
Мне действительно хочется спросить: «Какова ваша большая цель?» здесь может быть уместно. Я поклонник «ответа на вопрос, который был задан», но я не могу не чувствовать, что есть другой способ подойти к проблеме (в зависимости от того, что это такое).   -  person Marcel Wilson    schedule 14.10.2020
comment
@CharlesDuffy разве он не завершается автоматически, когда в подпроцессе возникает исключение?   -  person user1179317    schedule 14.10.2020
comment
@user1179317 user1179317, обычно, если он не переопределен (f / e, обработчиком исключений, который перехватывает исключение, печатает его, но затем продолжает выполнение). Но тот факт, что вы сочли необходимым и уместным написать этот код, подразумевает, что что-то было переопределено.   -  person Charles Duffy    schedule 14.10.2020
comment
@MarcelWilson Основная идея состоит в том, чтобы передавать результаты (stdout) в том виде, в каком они приходят пользователю. Затем, если в середине возникает исключение, чтобы также предоставить эту ошибку/информацию пользователю   -  person user1179317    schedule 14.10.2020
comment
Обычно обычной практикой является чтение stdout до конца, а затем, когда вы дойдете до конца, вызовите p.wait(), чтобы установить p.returncode.   -  person Charles Duffy    schedule 14.10.2020
comment
... если ваш дочерний процесс завершается, как только он видит исключение, это исключение не будет в середине, а будет в конце потока, поэтому вы можете перейти прямо из цикла for line in proc.self.stdout:, выходящего в p.wait(), и проверка p.returncode, как только wait вернется.   -  person Charles Duffy    schedule 14.10.2020
comment
Да, как я уже упоминал, на самом деле не нужно ждать до конца, пока пользователь что-нибудь получит, или не хотите буферизовать все в стандартный вывод, особенно если мы записываем тонны данных в стандартный вывод. По сути, хотелось бы сбросить его другому приложению или пользователю, когда мы пишем в стандартный вывод, в основном для минимизации использования памяти в этой среде. Мне не нужно использовать p.wait, если я использую p.communicate(), но я не хочу этого делать   -  person user1179317    schedule 14.10.2020
comment
@CharlesDuffy Да, я думаю, вы можете сказать, что это не в середине, а в конце   -  person user1179317    schedule 14.10.2020
comment
@user1179317 user1179317 У меня покалывает паучье чутье. Я мог бы быть совершенно неуместным, но запуск чего-то в подпроцессе, который может вызвать исключение, но все равно запускается, кажется неудобным. Я обычно думаю об исключении, которое возникает, как о чем-то, что сценарий должен поймать и обработать или выйти. Похоже, вы ловите его, печатаете трассировку стека, но затем все равно продолжаете. В этот момент вы технически «разобрались с этим». Что родительский процесс будет делать с повторным отловом этого исключения?   -  person Marcel Wilson    schedule 14.10.2020
comment
@MarcelWilson Хорошо, может быть, я не совсем понимаю. Но если в подпроцессе возникает исключение, я не хочу продолжать, я хочу, чтобы он вышел из подпроцесса и вызвал исключение. Что я действительно хочу, так это распечатать стандартный вывод из подпроцесса, как это происходит, затем, как только в подпроцессе возникнет исключение, выйти из подпроцесса, и родительский процесс также распечатает исключение, перейдите к очистке или, наконец, блок, то выполнение завершено.   -  person user1179317    schedule 14.10.2020
comment
@CharlesDuffy Извините, только что понял, что вы упомянули. Так что да, это действительно работает, если после того, как я пройду через стандартный вывод, у меня есть p.wait, а затем проверьте p.returncode   -  person user1179317    schedule 14.10.2020
comment
...учитывая это, есть ли здесь еще вопрос?   -  person Charles Duffy    schedule 14.10.2020
comment
@CharlesDuffy больше нет вопросов. Должен ли я тогда обновить свой вопрос вашим ответом?   -  person user1179317    schedule 14.10.2020
comment
Нет, ответы принадлежат ответам, а не вопросам. Я бы написал один, но я не уверен, что понимаю, почему изначально был вопрос достаточно хорошо, чтобы сделать это; не стесняйтесь добавлять свой собственный ответ (через кнопку «Отправить ответ»), используя все, что мои комментарии помогли вам узнать; после 3-дневного тайм-аута вы сможете принять его и, таким образом, отметить свой вопрос как решенный.   -  person Charles Duffy    schedule 14.10.2020


Ответы (1)


Вот ответ на мой вопрос ниже, действительно основанный на комментарии @CharlesDuffy:

Короче говоря, убедитесь, что stderr=subprocess.PIPE в классе ExtProcess, тогда ответ находится в версии № 3, где после итерации через стандартный вывод мы используем .wait() и returncode, чтобы проверить, была ли ошибка, если это так, создайте исключение, захватив ошибку из stderr.read() перехватываться в родительском/основном.

import subprocess

class ExtProcess():
    def __init__(self, *args):
        self.proc = subprocess.Popen(['python', *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    def __iter__(self):
        return self

    def __next__(self):
        while True:
            line = self.proc.stdout.readline()
            if not line:
                raise StopIteration
            return line


def run():
    ## version #1
    # reader = ExtProcess("sample_extproc.py")
    # for d in reader:
    #     print(f"got: {d}")

    ## version #2
    # proc = subprocess.Popen(['python', "sample_extproc.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    # output, error = proc.communicate()
    # print("got:", output)
    # if proc.returncode:
    #     raise Exception(error)

    ## version #3
    reader = ExtProcess("sample_extproc.py")
    for d in reader:
        print(f"got: {d}")
    reader.proc.wait()
    if reader.proc.returncode:
       raise Exception(reader.proc.stderr.read())

def main():
    try:
        print("start...")
        run()
        print("complete...")
    except Exception as e:
        print(f"Package midstream error here: {str(e)}")
    finally:
        print("clean up...")


if __name__ == "__main__":
    main()
person user1179317    schedule 14.10.2020
comment
Итак, единственное, что меня беспокоит, читая этот код, это то, что если ваша программа попытается записать больше контента, чем поместится в конвейер, в stderr до закрытия stdout, эта запись может заблокироваться, потому что ее никто не читает (stderr не только для ошибок - - это также то, к чему относится диагностический контент, такой как журналы); поэтому вы можете оказаться в тупике, если программа не закончит запись в стандартный вывод (или не попытается закрыть стандартный вывод) до тех пор, пока запись в стандартный вывод не завершится. - person Charles Duffy; 15.10.2020
comment
@CharlesDuffy Хм, не уверен, что я полностью понимаю, но есть ли тайм-аут чего-то, что я мог бы использовать? Я предполагаю, что всякий раз, когда я добираюсь до p.wait(), стандартный вывод достиг StopIteration, поэтому я не должен слишком долго ждать в p.wait() - person user1179317; 15.10.2020
comment
Потенциальная проблема заключается в взаимоблокировке дочернего процесса. Попробуйте использовать дочерний процесс, который записывает несколько килобайт данных в stderr во время выполнения — его попытка записать эти данные будет зависать, если что-то в родительском процессе активно не читает из stderr в это время; когда вы вообще не читаете из stderr до выхода из stdout, это означает, что вы не можете безопасно запускать дочерние процессы, которые записывают больше данных, чем буфер FIFO, в stderr, пока у них все еще открыт stdout. - person Charles Duffy; 15.10.2020
comment
... если ваш дочерний процесс застрял и не может завершить запись в stderr, это означает, что он никогда не заканчивает запись содержимого в stdout (и затем закрывает его), поэтому родительский процесс никогда не выходит из своего цикла. - person Charles Duffy; 15.10.2020
comment
Один из способов избежать этого — иметь отдельный поток, считывающий содержимое из stderr в буфер по мере его поступления. - person Charles Duffy; 15.10.2020
comment
Другой способ — использовать selectors; некоторые ответы в stackoverflow.com/questions/31833897/ перейдите в него. Достаточно просто прочитать из stderr в родительский процесс и сохранить данные для дальнейшего использования. - person Charles Duffy; 15.10.2020
comment
... смысл использования selectors для чтения из stdout и stderr одновременно, или потока, читающего из proc.stderr в фоновом режиме, и т. д., состоит в том, чтобы убедиться, что stderr потребляется, когда дочерний процесс записывает его, поэтому дочерний процесс не может зависнуть, пытаясь дождаться полного буфера FIFO, присоединенного к его stderr, чтобы было место для записи дополнительных данных (в то время как родитель не предпринимает никаких усилий для чтения и, таким образом, вообще очищает этот FIFO). - person Charles Duffy; 15.10.2020
comment
@CharlesDuffy имеет смысл, спасибо за подробное объяснение. Взглянем на селекторы или создадим отдельный поток, читающий stderr. Определенно ценю то, что приложили дополнительные усилия только к моему ответу и изучили потенциальные/дальнейшие проблемы. - person user1179317; 15.10.2020