web-dev-qa-db-fra.com

timeout sur le sous-processus readline dans python

J'ai un petit problème que je ne sais pas trop comment résoudre. Voici un exemple minimal:

Ce que j'ai

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

Ce que je voudrais

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)

Je lis une ligne d'un sous-processus et j'en fais quelque chose. Ce que je veux, c'est quitter si aucune ligne n'est arrivée après un intervalle de temps fixe. Des recommandations?

38
Tom

Merci pour toutes les réponses! J'ai trouvé un moyen de résoudre mon problème en utilisant simplement select.poll pour jeter un œil dans 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)
20
Tom

Voici une solution portable qui applique le délai d'attente pour la lecture d'une seule ligne en utilisant 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()
16
jfs

J'ai utilisé quelque chose d'un peu plus général dans python (IIRC aussi reconstitué à partir de SO questions, mais je ne me souviens pas lesquelles)).

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()

Soyez averti, cependant, cela utilise une interruption pour arrêter la fonction que vous lui donnez. Cela peut ne pas être une bonne idée pour toutes les fonctions et cela vous empêche également de fermer le programme avec ctrl + c pendant le timeout (c'est-à-dire que ctrl + c sera traité comme un timeout) Vous pouvez utiliser ceci comme un appel comme:

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)

Cela pourrait être un peu exagéré, cependant. Je soupçonne qu'il existe une option plus simple pour votre cas que je ne connais pas.

10
Flogo

Une solution portable consiste à utiliser un thread pour tuer le processus enfant si la lecture d'une ligne prend trop de temps:

#!/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 classe est comme threading.Timer pouvant être redémarré et/ou bloqué:

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()
4
jfs

Essayez d'utiliser 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

Quelques essais pour montrer que cela fonctionne:

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

Pour un exemple plus détaillé, voir pGuides .

4
AXE-Labs

Dans Python 3, une option de temporisation a été ajoutée au module de sous-processus. À l'aide d'une structure comme

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

analyze(o)

serait une bonne solution.

Étant donné que la sortie devrait contenir un nouveau caractère de ligne, il est sûr de supposer qu'il s'agit de texte (comme dans imprimable, lisible), auquel cas universal_newlines=True le drapeau est fortement recommandé.

Si Python2 est un must, veuillez utiliser https://pypi.python.org/pypi/subprocess32/ (backport)

Pour un python pur Python 2, regardez tilisation du module 'sous-processus' avec timeout .

3

pendant que votre solution (de Tom) fonctionne, l'utilisation de select() dans l'idiome C est plus compacte. c'est l'équivalent de votre réponse

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]

le reste est le même.

voir pydoc select.select.

[Remarque: ceci est spécifique à Unix, comme le sont certaines des autres réponses.]

[Remarque 2: modifié pour ajouter une mise en mémoire tampon de ligne conformément à la demande OP]

[Remarque 3: la mise en mémoire tampon de la ligne peut ne pas être fiable dans toutes les circonstances, ce qui entraîne un blocage de readline ()]

1
jcomeau_ictx