web-dev-qa-db-fra.com

Utilisation du module 'sous-processus' avec timeout

Voici le code Python permettant d'exécuter une commande arbitraire renvoyant ses données stdout ou de déclencher une exception sur des codes de sortie non nuls:

proc = subprocess.Popen(
    cmd,
    stderr=subprocess.STDOUT,  # Merge stdout and stderr
    stdout=subprocess.PIPE,
    Shell=True)

communicate est utilisé pour attendre la fin du processus:

stdoutdata, stderrdata = proc.communicate()

Le module subprocess ne prend pas en charge le délai d'attente - possibilité de tuer un processus exécuté pendant plus de X secondes - par conséquent, communicate peut prendre une éternité.

Quel est le moyen le plus simple d'implémenter les délais dans un programme Python destiné à s'exécuter sous Windows et Linux?

274
Sridhar Ratnakumar

En Python 3.3+:

from subprocess import STDOUT, check_output

output = check_output(cmd, stderr=STDOUT, timeout=seconds)

output est une chaîne d'octets contenant les données stderr, fusionnées et fusionnées de la commande. 

Ce code déclenche CalledProcessError sur un statut de sortie autre que zéro, comme spécifié dans le texte de la question, contrairement à la méthode proc.communicate().

J'ai supprimé Shell=True car il est souvent utilisé inutilement. Vous pouvez toujours le rajouter si cmd l'exige effectivement. Si vous ajoutez Shell=True, c’est-à-dire si le processus enfant génère ses propres descendants; check_output() peut revenir beaucoup plus tard que le délai imparti, voir Échec du délai d'attente du sous-processus .

La fonctionnalité de délai d'attente est disponible sur Python 2.x via le répertoire subprocess32 du module de sous-processus 3.2+.

135
jfs

Je ne connais pas grand chose aux détails de bas niveau. mais, étant donné que dans python 2.6, l’API offre la possibilité d’attendre les threads et de terminer les processus .__, qu’en est-il de l’exécuter dans un thread séparé

import subprocess, threading

class Command(object):
    def __init__(self, cmd):
        self.cmd = cmd
        self.process = None

    def run(self, timeout):
        def target():
            print 'Thread started'
            self.process = subprocess.Popen(self.cmd, Shell=True)
            self.process.communicate()
            print 'Thread finished'

        thread = threading.Thread(target=target)
        thread.start()

        thread.join(timeout)
        if thread.is_alive():
            print 'Terminating process'
            self.process.terminate()
            thread.join()
        print self.process.returncode

command = Command("echo 'Process started'; sleep 2; echo 'Process finished'")
command.run(timeout=3)
command.run(timeout=1)

La sortie de cet extrait de code sur ma machine est la suivante:

Thread started
Process started
Process finished
Thread finished
0
Thread started
Process started
Terminating process
Thread finished
-15

où on peut voir que, dans la première exécution, le processus s'est terminé correctement (code retour 0), tandis que dans la seconde, le processus a été terminé (code retour -15).

Je n'ai pas testé sous Windows; mais, en dehors de la mise à jour de l'exemple de commande , je pense que cela devrait fonctionner car je n'ai trouvé dans la documentation rien qui indique que thread.join ou process.terminate n'est pas pris en charge.

193
jcollado

la réponse de jcollado peut être simplifiée à l’aide du threading.Timer class:

import shlex
from subprocess import Popen, PIPE
from threading import Timer

def run(cmd, timeout_sec):
    proc = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
    timer = Timer(timeout_sec, proc.kill)
    try:
        timer.start()
        stdout, stderr = proc.communicate()
    finally:
        timer.cancel()

# Examples: both take 1 second
run("sleep 1", 5)  # process ends normally at 1 second
run("sleep 5", 1)  # timeout happens at 1 second
103
sussudio

Si vous êtes sur Unix,

import signal
  ...
class Alarm(Exception):
    pass

def alarm_handler(signum, frame):
    raise Alarm

signal.signal(signal.SIGALRM, alarm_handler)
signal.alarm(5*60)  # 5 minutes
try:
    stdoutdata, stderrdata = proc.communicate()
    signal.alarm(0)  # reset the alarm
except Alarm:
    print "Oops, taking too long!"
    # whatever else
78
Alex Martelli

Voici la solution d'Alex Martelli en tant que module permettant de tuer correctement les processus. Les autres approches ne fonctionnent pas car elles n'utilisent pas proc.communicate (). Donc, si vous avez un processus qui produit beaucoup de sortie, il remplira son tampon de sortie puis se bloquera jusqu'à ce que vous lisiez quelque chose dessus.

from os import kill
from signal import alarm, signal, SIGALRM, SIGKILL
from subprocess import PIPE, Popen

def run(args, cwd = None, Shell = False, kill_tree = True, timeout = -1, env = None):
    '''
    Run a command with a timeout after which it will be forcibly
    killed.
    '''
    class Alarm(Exception):
        pass
    def alarm_handler(signum, frame):
        raise Alarm
    p = Popen(args, Shell = Shell, cwd = cwd, stdout = PIPE, stderr = PIPE, env = env)
    if timeout != -1:
        signal(SIGALRM, alarm_handler)
        alarm(timeout)
    try:
        stdout, stderr = p.communicate()
        if timeout != -1:
            alarm(0)
    except Alarm:
        pids = [p.pid]
        if kill_tree:
            pids.extend(get_process_children(p.pid))
        for pid in pids:
            # process might have died before getting to this line
            # so wrap to avoid OSError: no such process
            try: 
                kill(pid, SIGKILL)
            except OSError:
                pass
        return -9, '', ''
    return p.returncode, stdout, stderr

def get_process_children(pid):
    p = Popen('ps --no-headers -o pid --ppid %d' % pid, Shell = True,
              stdout = PIPE, stderr = PIPE)
    stdout, stderr = p.communicate()
    return [int(p) for p in stdout.split()]

if __== '__main__':
    print run('find /', Shell = True, timeout = 3)
    print run('find', Shell = True)
43
Björn Lindqvist

J'ai modifié sussudio answer. La fonction retourne maintenant: (returncode, stdout, stderr, timeout) - stdout et stderr est décodé en chaîne utf-8

def kill_proc(proc, timeout):
  timeout["value"] = True
  proc.kill()

def run(cmd, timeout_sec):
  proc = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  timeout = {"value": False}
  timer = Timer(timeout_sec, kill_proc, [proc, timeout])
  timer.start()
  stdout, stderr = proc.communicate()
  timer.cancel()
  return proc.returncode, stdout.decode("utf-8"), stderr.decode("utf-8"), timeout["value"]
16
Michal Z m u d a

personne surpris a mentionné l'utilisation de timeout

timeout 5 ping -c 3 somehost

Cela ne fonctionnera évidemment pas pour tous les cas d'utilisation, mais si vous utilisez un script simple, il est difficile à battre.

Egalement disponible en tant que gtimeout dans coreutils via homebrew pour les utilisateurs mac.

15
Karsten

Une autre option consiste à écrire dans un fichier temporaire pour empêcher le blocage de stdout au lieu de devoir interroger avec communic (). Cela a fonctionné pour moi là où les autres réponses n’ont pas fonctionné; par exemple sur windows.

    outFile =  tempfile.SpooledTemporaryFile() 
    errFile =   tempfile.SpooledTemporaryFile() 
    proc = subprocess.Popen(args, stderr=errFile, stdout=outFile, universal_newlines=False)
    wait_remaining_sec = timeout

    while proc.poll() is None and wait_remaining_sec > 0:
        time.sleep(1)
        wait_remaining_sec -= 1

    if wait_remaining_sec <= 0:
        killProc(proc.pid)
        raise ProcessIncompleteError(proc, timeout)

    # read temp streams from start
    outFile.seek(0);
    errFile.seek(0);
    out = outFile.read()
    err = errFile.read()
    outFile.close()
    errFile.close()
10
Matt

timeout est maintenant supporté par call() et communicate() dans le module de sous-processus (à partir de Python3.3):

import subprocess

subprocess.call("command", timeout=20, Shell=True)

Cela va appeler la commande et lever l'exception

subprocess.TimeoutExpired

si la commande ne se termine pas après 20 secondes.

Vous pouvez ensuite gérer l'exception pour continuer votre code, quelque chose comme:

try:
    subprocess.call("command", timeout=20, Shell=True)
except subprocess.TimeoutExpired:
    # insert code here

J'espère que cela t'aides.

8
James

Voici ma solution, j'utilisais Thread and Event:

import subprocess
from threading import Thread, Event

def kill_on_timeout(done, timeout, proc):
    if not done.wait(timeout):
        proc.kill()

def exec_command(command, timeout):

    done = Event()
    proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    watcher = Thread(target=kill_on_timeout, args=(done, timeout, proc))
    watcher.daemon = True
    watcher.start()

    data, stderr = proc.communicate()
    done.set()

    return data, stderr, proc.returncode

En action:

In [2]: exec_command(['sleep', '10'], 5)
Out[2]: ('', '', -9)

In [3]: exec_command(['sleep', '10'], 11)
Out[3]: ('', '', 0)
5
rsk

La solution que j'utilise consiste à préfixer la commande Shell avec timelimit . Si la commande prend trop de temps, timelimit l'arrêtera et Popen aura un code de retour défini par timelimit. Si elle est> 128, cela signifie que le délai imparti a tué le processus.

Voir aussi sous-processus python avec timeout et sortie importante (> 64 Ko)

4
bortzmeyer

J'ai ajouté la solution avec thread de jcollado à mon module Python easyprocess .

Installer:

pip install easyprocess

Exemple:

from easyprocess import Proc

# Shell is not supported!
stdout=Proc('ping localhost').call(timeout=1.5).stdout
print stdout
4
ponty

si vous utilisez python 2, essayez-le

import subprocess32

try:
    output = subprocess32.check_output(command, Shell=True, timeout=3)
except subprocess32.TimeoutExpired as e:
    print e
3
ThOong Ku

Je ne sais pas pourquoi cela n'est pas mentionné, mais depuis Python 3.5, il existe une nouvelle commande subprocess.run universal (destinée à remplacer check_call, check_output ...) et qui comporte également le paramètre timeout.

subprocess.run (arguments, *, stdin = Aucun, entrée = Aucun, stdout = Aucun, stderr = Aucun, Shell = Faux, cwd = Aucun, délai d'attente = Aucun, cocher = Faux, codage = Aucun, erreurs = Aucun)

Run the command described by args. Wait for command to complete, then return a CompletedProcess instance.

Il déclenche une exception subprocess.TimeoutExpired à l'expiration du délai d'attente.

3

Une fois que vous avez compris le fonctionnement complet des machines dans * unix, vous trouverez facilement une solution plus simple:

Prenons cet exemple simple, qui explique comment rendre methode de communication () methode timeoutable en utilisant select.select () (disponible presque partout sur * nix de nos jours). Cela peut aussi être écrit avec epoll/poll/kqueue, mais la variante select.select () pourrait être un bon exemple pour vous. Et les principales limitations de select.select () (vitesse et 1024 FDS max) ne sont pas applicables pour votre tâche.

Cela fonctionne sous * nix, ne crée pas de threads, n’utilise pas de signaux, peut être lancé depuis n’importe quel thread (pas seulement principal) et rapide pour lire 250 Mo/s de données depuis stdout sur ma machine (i5 2.3ghz).

Il y a un problème pour joindre stdout/stderr à la fin de la communication. Si vous avez une sortie de programme énorme, cela pourrait entraîner une utilisation importante de la mémoire. Mais vous pouvez appeler plusieurs fois plusieurs fois avec plusieurs temps morts.

class Popen(subprocess.Popen):
    def communicate(self, input=None, timeout=None):
        if timeout is None:
            return subprocess.Popen.communicate(self, input)

        if self.stdin:
            # Flush stdio buffer, this might block if user
            # has been writing to .stdin in an uncontrolled
            # fashion.
            self.stdin.flush()
            if not input:
                self.stdin.close()

        read_set, write_set = [], []
        stdout = stderr = None

        if self.stdin and input:
            write_set.append(self.stdin)
        if self.stdout:
            read_set.append(self.stdout)
            stdout = []
        if self.stderr:
            read_set.append(self.stderr)
            stderr = []

        input_offset = 0
        deadline = time.time() + timeout

        while read_set or write_set:
            try:
                rlist, wlist, xlist = select.select(read_set, write_set, [], max(0, deadline - time.time()))
            except select.error as ex:
                if ex.args[0] == errno.EINTR:
                    continue
                raise

            if not (rlist or wlist):
                # Just break if timeout
                # Since we do not close stdout/stderr/stdin, we can call
                # communicate() several times reading data by smaller pieces.
                break

            if self.stdin in wlist:
                chunk = input[input_offset:input_offset + subprocess._PIPE_BUF]
                try:
                    bytes_written = os.write(self.stdin.fileno(), chunk)
                except OSError as ex:
                    if ex.errno == errno.EPIPE:
                        self.stdin.close()
                        write_set.remove(self.stdin)
                    else:
                        raise
                else:
                    input_offset += bytes_written
                    if input_offset >= len(input):
                        self.stdin.close()
                        write_set.remove(self.stdin)

            # Read stdout / stderr by 1024 bytes
            for fn, tgt in (
                (self.stdout, stdout),
                (self.stderr, stderr),
            ):
                if fn in rlist:
                    data = os.read(fn.fileno(), 1024)
                    if data == '':
                        fn.close()
                        read_set.remove(fn)
                    tgt.append(data)

        if stdout is not None:
            stdout = ''.join(stdout)
        if stderr is not None:
            stderr = ''.join(stderr)

        return (stdout, stderr)
2
Vadim Fint

Prévendre la commande Linux timeout n’est pas une solution de contournement mauvaise et cela a fonctionné pour moi.

cmd = "timeout 20 "+ cmd
subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
2
Vikram Hosakote

Vous pouvez le faire en utilisant select

import subprocess
from datetime import datetime
from select import select

def call_with_timeout(cmd, timeout):
    started = datetime.now()
    sp = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    while True:
        p = select([sp.stdout], [], [], timeout)
        if p[0]:
            p[0][0].read()
        ret = sp.poll()
        if ret is not None:
            return ret
        if (datetime.now()-started).total_seconds() > timeout:
            sp.kill()
            return None
2
dspeyer

J'ai mis en œuvre ce que je pourrais rassembler de quelques-uns d'entre eux. Cela fonctionne sous Windows, et comme il s'agit d'un wiki de communauté, je pense que je partagerais aussi mon code:

class Command(threading.Thread):
    def __init__(self, cmd, outFile, errFile, timeout):
        threading.Thread.__init__(self)
        self.cmd = cmd
        self.process = None
        self.outFile = outFile
        self.errFile = errFile
        self.timed_out = False
        self.timeout = timeout

    def run(self):
        self.process = subprocess.Popen(self.cmd, stdout = self.outFile, \
            stderr = self.errFile)

        while (self.process.poll() is None and self.timeout > 0):
            time.sleep(1)
            self.timeout -= 1

        if not self.timeout > 0:
            self.process.terminate()
            self.timed_out = True
        else:
            self.timed_out = False

Puis d'une autre classe ou d'un autre fichier:

        outFile =  tempfile.SpooledTemporaryFile()
        errFile =   tempfile.SpooledTemporaryFile()

        executor = command.Command(c, outFile, errFile, timeout)
        executor.daemon = True
        executor.start()

        executor.join()
        if executor.timed_out:
            out = 'timed out'
        else:
            outFile.seek(0)
            errFile.seek(0)
            out = outFile.read()
            err = errFile.read()

        outFile.close()
        errFile.close()
2
joslinm

Bien que je n'y aie pas beaucoup réfléchi, ce décorateur que j'ai trouvé chez ActiveState semble très utile pour ce genre de chose. Avec subprocess.Popen(..., close_fds=True), au moins, je suis prêt pour le script shell en Python.

1
Ehtesh Choudhury

J'ai eu le problème que je voulais mettre fin à un sous-processus multithreading s'il prenait plus de temps qu'un délai donné. Je voulais définir un délai d'attente dans Popen(), mais cela n'a pas fonctionné. Ensuite, j'ai réalisé que Popen().wait() est égal à call() et j'ai donc eu l'idée de définir un délai d'attente dans la méthode .wait(timeout=xxx), qui a finalement fonctionné. Ainsi, je l'ai résolu de cette façon:

import os
import sys
import signal
import subprocess
from multiprocessing import Pool

cores_for_parallelization = 4
timeout_time = 15  # seconds

def main():
    jobs = [...YOUR_JOB_LIST...]
    with Pool(cores_for_parallelization) as p:
        p.map(run_parallel_jobs, jobs)

def run_parallel_jobs(args):
    # Define the arguments including the paths
    initial_terminal_command = 'C:\\Python34\\python.exe'  # Python executable
    function_to_start = 'C:\\temp\\xyz.py'  # The multithreading script
    final_list = [initial_terminal_command, function_to_start]
    final_list.extend(args)

    # Start the subprocess and determine the process PID
    subp = subprocess.Popen(final_list)  # starts the process
    pid = subp.pid

    # Wait until the return code returns from the function by considering the timeout. 
    # If not, terminate the process.
    try:
        returncode = subp.wait(timeout=timeout_time)  # should be zero if accomplished
    except subprocess.TimeoutExpired:
        # Distinguish between Linux and Windows and terminate the process if 
        # the timeout has been expired
        if sys.platform == 'linux2':
            os.kill(pid, signal.SIGTERM)
        Elif sys.platform == 'win32':
            subp.terminate()

if __== '__main__':
    main()
1
Joram Schito

Il y a une idée pour sous-classer la classe Popen et l'étendre avec quelques décorateurs de méthodes simples. Appelons cela ExpirablePopen.

from logging import error
from subprocess import Popen
from threading import Event
from threading import Thread


class ExpirablePopen(Popen):

    def __init__(self, *args, **kwargs):
        self.timeout = kwargs.pop('timeout', 0)
        self.timer = None
        self.done = Event()

        Popen.__init__(self, *args, **kwargs)

    def __tkill(self):
        timeout = self.timeout
        if not self.done.wait(timeout):
            error('Terminating process {} by timeout of {} secs.'.format(self.pid, timeout))
            self.kill()

    def expirable(func):
        def wrapper(self, *args, **kwargs):
            # zero timeout means call of parent method
            if self.timeout == 0:
                return func(self, *args, **kwargs)

            # if timer is None, need to start it
            if self.timer is None:
                self.timer = thr = Thread(target=self.__tkill)
                thr.daemon = True
                thr.start()

            result = func(self, *args, **kwargs)
            self.done.set()

            return result
        return wrapper

    wait = expirable(Popen.wait)
    communicate = expirable(Popen.communicate)


if __== '__main__':
    from subprocess import PIPE

    print ExpirablePopen('ssh -T [email protected]', stdout=PIPE, timeout=1).communicate()
1
Alexander Yakovlev

J'ai utilisé killableprocess avec succès sous Windows, Linux et Mac. Si vous utilisez Cygwin Python, vous aurez besoin de la version du processus killable de OSAF car, sinon, les processus Windows natifs ne seront pas tués.

1
Heikki Toivonen

Cette solution supprime l’arbre de processus si Shell = True, transmet des paramètres au processus (ou non), a un délai d’expiration et récupère les sorties stdout, stderr et process du rappel (elle utilise psutil pour kill_proc_tree). Cela reposait sur plusieurs solutions publiées dans SO, notamment jcollado. Publication en réponse aux commentaires d'Anson et de jradice dans la réponse de jcollado. Testé sous Windows Srvr 2012 et Ubuntu 14.04. Veuillez noter que pour Ubuntu, vous devez changer l'appel parent.children (...) en parent.get_children (...).

def kill_proc_tree(pid, including_parent=True):
  parent = psutil.Process(pid)
  children = parent.children(recursive=True)
  for child in children:
    child.kill()
  psutil.wait_procs(children, timeout=5)
  if including_parent:
    parent.kill()
    parent.wait(5)

def run_with_timeout(cmd, current_dir, cmd_parms, timeout):
  def target():
    process = subprocess.Popen(cmd, cwd=current_dir, Shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)

    # wait for the process to terminate
    if (cmd_parms == ""):
      out, err = process.communicate()
    else:
      out, err = process.communicate(cmd_parms)
    errcode = process.returncode

  thread = Thread(target=target)
  thread.start()

  thread.join(timeout)
  if thread.is_alive():
    me = os.getpid()
    kill_proc_tree(me, including_parent=False)
    thread.join()
0
Tomas

pour Python 2.6+, utilisez gevent

 from gevent.subprocess import Popen, PIPE, STDOUT

 def call_sys(cmd, timeout):
      p= Popen(cmd, Shell=True, stdout=PIPE)
      output, _ = p.communicate(timeout=timeout)
      assert p.returncode == 0, p. returncode
      return output

 call_sys('./t.sh', 2)

 # t.sh example
 sleep 5
 echo done
 exit 1
0
whi

https://pypi.python.org/pypi/python-subprocess2 fournit des extensions du module de sous-processus qui vous permettent d’attendre jusqu’à un certain délai, sinon mettre fin. 

Donc, attendre 10 secondes que le processus se termine, sinon, tuez:

pipe  = subprocess.Popen('...')

timeout =  10

results = pipe.waitOrTerminate(timeout)

Ceci est compatible avec Windows et Unix. "résultats" est un dictionnaire, il contient "returnCode" qui est le retour de l'application (ou None si elle devait être tuée), ainsi que "actionTaken". qui sera "SUBPROCESS2_PROCESS_COMPLETED" si le processus s'est déroulé normalement, ou un masque de "SUBPROCESS2_PROCESS_TERMINATED" et de SUBPROCESS2_PROCESS_KILLED en fonction de l'action entreprise (voir la documentation pour plus de détails)

0
Tim Savannah

Malheureusement, je suis lié par des règles très strictes concernant la divulgation du code source par mon employeur. Je ne peux donc pas fournir de code. Mais à mon goût, la meilleure solution consiste à créer une sous-classe remplaçant Popen.wait() pour interroger au lieu d'attendre indéfiniment, et Popen.__init__ pour accepter un paramètre de délai d'attente. Une fois que vous avez fait cela, toutes les autres méthodes Popen (qui appellent wait) fonctionneront comme prévu, y compris communicate.

0

python 2.7

import time
import subprocess

def run_command(cmd, timeout=0):
    start_time = time.time()
    df = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    while timeout and df.poll() == None:
        if time.time()-start_time >= timeout:
            df.kill()
            return -1, ""
    output = '\n'.join(df.communicate()).strip()
    return df.returncode, output
0
Joy Wang