таймаут на подпроцессе 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) в 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) 

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

Спасибо за ответы на все вопросы! Я нашел способ решить свою проблему, просто используя 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) 

Я использовал что-то более общее в 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) в 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) 

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

Вот портативное решение, которое обеспечивает тайм-аут для чтения одной строки с использованием 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() - #!/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() 

В 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 с чистым питоном рассмотрите раздел «Подпроцесс» модуля с тайм-аутом .

Попробуйте использовать 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 .

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

 #!/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() 

в то время как ваше (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 ()]