Подтвердить что ты не робот

Таймаут на подпроцессе readline в python

У меня есть небольшая проблема, и я не совсем уверен, как ее решить. Вот минимальный пример:

Что у меня

scan_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while(some_criterium):
    line = scan_process.stdout.readline()
    some_criterium = do_something(line)

Что мне хотелось бы

scan_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while(some_criterium):
    line = scan_process.stdout.readline()
    if nothing_happens_after_10s:
        break
    else:
        some_criterium = do_something(line)

Я читаю строку из подпроцесса и делаю что-то с ней. Я хочу, чтобы выйти, если после фиксированного интервала времени не было линии. Любые рекомендации?

4b9b3361

Ответ 1

Спасибо за ответы! Я нашел способ решить свою проблему, просто используя select.poll, чтобы заглянуть в stdout.

import select
...
scan_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
poll_obj = select.poll()
poll_obj.register(scan_process.stdout, select.POLLIN)   
while(some_criterium and not time_limit):
    poll_result = poll_obj.poll(0)
    if poll_result:
        line = scan_process.stdout.readline()
        some_criterium = do_something(line)
    update(time_limit)

Ответ 2

Я использовал что-то более общее в python (IIRC также собрано из вопросов SO, но я не могу вспомнить, какие из них).

import thread
from threading import Timer

def run_with_timeout(timeout, default, f, *args, **kwargs):
    if not timeout:
        return f(*args, **kwargs)
    try:
        timeout_timer = Timer(timeout, thread.interrupt_main)
        timeout_timer.start()
        result = f(*args, **kwargs)
        return result
    except KeyboardInterrupt:
        return default
    finally:
        timeout_timer.cancel()

Будем предупреждать, однако, это использует прерывание, чтобы остановить любую функцию, которую вы ему даете. Это не может быть хорошей идеей для всех функций, а также не позволяет вам закрывать программу ctrl + c во время таймаута (т.е. Ctrl + c будет обрабатываться как тайм-аут) Вы можете использовать это для вызова:

scan_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while(some_criterium):
    line = run_with_timeout(timeout, None, scan_process.stdout.readline)
    if line is None:
        break
    else:
        some_criterium = do_something(line)

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

Ответ 3

Здесь представлено портативное решение, которое обеспечивает тайм-аут для чтения одной строки с помощью asyncio:

#!/usr/bin/env python3
import asyncio
import sys
from asyncio.subprocess import PIPE, STDOUT

async def run_command(*args, timeout=None):
    # start child process
    # NOTE: universal_newlines parameter is not supported
    process = await asyncio.create_subprocess_exec(*args,
            stdout=PIPE, stderr=STDOUT)

    # read line (sequence of bytes ending with b'\n') asynchronously
    while True:
        try:
            line = await asyncio.wait_for(process.stdout.readline(), timeout)
        except asyncio.TimeoutError:
            pass
        else:
            if not line: # EOF
                break
            elif do_something(line): 
                continue # while some criterium is satisfied
        process.kill() # timeout or some criterium is not satisfied
        break
    return await process.wait() # wait for the child process to exit


if sys.platform == "win32":
    loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows
    asyncio.set_event_loop(loop)
else:
    loop = asyncio.get_event_loop()

returncode = loop.run_until_complete(run_command("cmd", "arg 1", "arg 2",
                                                 timeout=10))
loop.close()

Ответ 4

В Python 3 в модуль подпроцесса добавлен параметр таймаута. Используя структуру типа

try:
    o, e = process.communicate(timeout=10)
except TimeoutExpired:
    process.kill()
    o, e = process.communicate()

analyze(o)

будет правильным решением.

Поскольку ожидается, что вывод будет содержать новый символ строки, можно с уверенностью предположить, что это текст (как в печатном, читаемом), и в этом случае настоятельно рекомендуется флаг universal_newlines=True.

Если Python2 является обязательным, используйте https://pypi.python.org/pypi/subprocess32/ (backport)

Для решения Python 2 с чистым питоном рассмотрим использование подпроцесса модуля с тайм-аутом.

Ответ 5

Попробуйте использовать signal.alarm:

#timeout.py
import signal,sys

def timeout(sig,frm):
  print "This is taking too long..."
  sys.exit(1)

signal.signal(signal.SIGALRM, timeout)
signal.alarm(10)
byte=0

while 'IT' not in open('/dev/urandom').read(2):
  byte+=2
print "I got IT in %s byte(s)!" % byte

Несколько прогонов, чтобы показать, что это работает:

$ python timeout.py 
This is taking too long...
$ python timeout.py 
I got IT in 4672 byte(s)!

Подробнее см. pGuides.

Ответ 6

Портативное решение - использовать поток, чтобы убить дочерний процесс, если чтение строки занимает слишком много времени:

#!/usr/bin/env python3
from subprocess import Popen, PIPE, STDOUT

timeout = 10
with Popen(command, stdout=PIPE, stderr=STDOUT,
           universal_newlines=True) as process:  # text mode
    # kill process in timeout seconds unless the timer is restarted
    watchdog = WatchdogTimer(timeout, callback=process.kill, daemon=True)
    watchdog.start()
    for line in process.stdout:
        # don't invoke the watcthdog callback if do_something() takes too long
        with watchdog.blocked:
            if not do_something(line):  # some criterium is not satisfied
                process.kill()
                break
            watchdog.restart()  # restart timer just before reading the next line
    watchdog.cancel()

где WatchdogTimer класс похож на threading.Timer, который можно перезапустить и/или заблокировать:

from threading import Event, Lock, Thread
from subprocess import Popen, PIPE, STDOUT
from time import monotonic  # use time.time or monotonic.monotonic on Python 2

class WatchdogTimer(Thread):
    """Run *callback* in *timeout* seconds unless the timer is restarted."""

    def __init__(self, timeout, callback, *args, timer=monotonic, **kwargs):
        super().__init__(**kwargs)
        self.timeout = timeout
        self.callback = callback
        self.args = args
        self.timer = timer
        self.cancelled = Event()
        self.blocked = Lock()

    def run(self):
        self.restart() # don't start timer until `.start()` is called
        # wait until timeout happens or the timer is canceled
        while not self.cancelled.wait(self.deadline - self.timer()):
            # don't test the timeout while something else holds the lock
            # allow the timer to be restarted while blocked
            with self.blocked:
                if self.deadline <= self.timer() and not self.cancelled.is_set():
                    return self.callback(*self.args)  # on timeout

    def restart(self):
        """Restart the watchdog timer."""
        self.deadline = self.timer() + self.timeout

    def cancel(self):
        self.cancelled.set()

Ответ 7

в то время как ваше (Tom's) решение работает, использование select() в C идиоме более компактно. это эквивалент вашего ответа

from select import select
scan_process = subprocess.Popen(command, 
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT,
                                bufsize=1)  # line buffered
while some_criterium and not time_limit:
    poll_result = select([scan_process.stdout], [], [], time_limit)[0]

остальное то же самое.

см. pydoc select.select.

[Примечание: это зависит от Unix, как и некоторые другие ответы.]

[Примечание 2: отредактировано для добавления буферизации строк в соответствии с запросом OP]

[Примечание 3: буферизация строк может быть ненадежной при любых обстоятельствах, что приводит к блокировке readline()]