Utilisation du module «sous-processus» avec timeout

325

Voici le code Python pour exécuter une commande arbitraire renvoyant ses stdoutdonnées, ou déclencher une exception sur les 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 subprocessmodule ne prend pas en charge le délai d'expiration - possibilité de tuer un processus en cours d'exécution pendant plus de X secondes - par conséquent, l' communicateexécution peut prendre une éternité.

Quelle est la manière la plus simple d'implémenter des délais d'attente dans un programme Python destiné à s'exécuter sur Windows et Linux?

Sridhar Ratnakumar
la source
2
Une entrée de suivi des problèmes Python: bugs.python.org/issue5673
Sridhar Ratnakumar
10
Utilisez pypi.python.org/pypi/subprocess32 pour Python2.x. C'est un backport de Python 3.x. Il a l'argument timeout pour call () et wait ().
guettli
1
pypi.python.org/pypi/subprocess32 ne fonctionne pas sous Windows :(
adrianX

Réponses:

170

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 qui contient les données stderout, stderr fusionnées de la commande.

check_outputaugmente CalledProcessErrorle statut de sortie non nul comme spécifié dans le texte de la question contrairement à la proc.communicate()méthode.

J'ai supprimé shell=Truecar il est souvent utilisé inutilement. Vous pouvez toujours le rajouter si cela est cmdvraiment nécessaire. Si vous ajoutez shell=Trueie, si le processus enfant engendre ses propres descendants; check_output()peut retourner beaucoup plus tard que le délai indiqué, voir Échec du délai d'expiration du sous - processus .

La fonction de timeout est disponible sur Python 2.x via le subprocess32backport du module de sous-processus 3.2+.

jfs
la source
17
En effet, et la prise en charge du délai d'expiration du sous-processus existe dans le backport du sous-processus que je maintiens pour une utilisation sur Python 2. pypi.python.org/pypi/subprocess32
gps
8
@gps Sridhar a demandé une solution multiplateforme, alors que votre backport ne prend en charge que POSIX: quand je l'ai essayé, MSVC s'est plaint (attendu) de manquer unistd.h :)
Shmil The Cat
Si vous n'avez pas besoin de la sortie, vous pouvez simplement utiliser le subprocess.call.
Kyle Gibson
Depuis Python3.5, utilisez subprocess.run () avec capture_output = True et utilisez le paramètre d'encodage pour obtenir une sortie utile.
MKesper
1
@MKesper: 1- check_output()est le moyen préféré pour obtenir la sortie (il renvoie la sortie directement, n'ignore pas les erreurs, il est disponible depuis toujours). 2- run()est plus flexible mais run()ignore l'erreur par défaut et nécessite des étapes supplémentaires pour obtenir la sortie 3- check_output()est implémenté en termes derun() et accepte donc la plupart des mêmes arguments. 4- nit: capture_outputest disponible depuis la 3.7, pas la 3.5
jfs
205

Je ne connais pas grand-chose aux détails de bas niveau; mais, étant donné qu'en python 2.6, l'API offre la possibilité d'attendre les threads et de terminer les processus, qu'en est-il de l'exécution du processus 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 dans ma machine est:

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

où l'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, à part mettre à jour l'exemple de commande, je pense que cela devrait fonctionner car je n'ai trouvé dans la documentation rien qui dise que thread.join ou process.terminate n'est pas pris en charge.

jcollado
la source
16
+1 Pour être indépendant de la plateforme. Je l'ai exécuté à la fois sur Linux et Windows 7 (cygwin et python Windows ordinaire) - fonctionne comme prévu dans les trois cas.
phooji
7
J'ai un peu modifié votre code afin de pouvoir passer les kwargs Popen natifs et les mettre sur gist. Il est maintenant prêt à l'emploi à usages multiples; gist.github.com/1306188
kirpit
2
Pour toute personne ayant le problème rencontré par @redice, cette question peut aider. En bref, si vous utilisez shell = True, le shell devient le processus enfant qui est tué et sa commande (enfant du processus enfant) perdure.
Anson
6
Cette réponse ne fournit pas la même fonctionnalité que l'original car elle ne renvoie pas stdout.
stephenbez
2
thread.is_alive peut conduire à une condition de concurrence. Voir ostricher.com/2015/01/python-subprocess-with-timeout
ChaimKut
132

La réponse de jcollado peut être simplifiée en utilisant la classe threading.Timer :

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
sussudio
la source
11
+1 pour une solution portable simple. Vous n'avez pas besoin lambda:t = Timer(timeout, proc.kill)
jfs
3
+1 Cela devrait être la réponse acceptée, car elle ne nécessite pas de modifier la façon dont le processus est lancé.
Dave Branton,
1
Pourquoi faut-il le lambda? La méthode liée p.kill ne pourrait-elle pas être utilisée sans le lambda?
Danny Staple
//, Seriez-vous prêt à inclure un exemple de son utilisation?
Nathan Basanese
1
@tuk timer.isAlive()avant timer.cancel()signifie que cela s'est terminé normalement
Charles
83

Si vous êtes sous 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
Alex Martelli
la source
3
Eh bien, je suis intéressé par une solution multiplateforme qui fonctionne au moins sur win / linux / mac.
Sridhar Ratnakumar
1
J'aime cette approche basée sur Unix. Idéalement, on combinerait cela avec une approche spécifique à Windows (en utilisant CreateProcess et Jobs) .. mais pour l'instant, la solution ci-dessous est simple, facile et fonctionne jusqu'à présent.
Sridhar Ratnakumar
3
J'ai ajouté une solution portable, voir ma réponse
flybywire
4
Cette solution ne fonctionnerait que si signal.signal (signal.SIGALARM, alarm_handler) est appelé à partir du thread principal. Voir la documentation pour signal
volatilevoid
Malheureusement, lors de l'exécution (sous linux) dans le contexte d'un module Apache (comme mod_python, mod_perl ou mod_php), j'ai trouvé que l'utilisation des signaux et des alarmes était interdite (probablement parce qu'ils interfèrent avec la logique IPC d'Apache). Donc, pour atteindre l'objectif de temporisation d'une commande, j'ai été obligé d'écrire des "boucles parentales" qui lancent un processus enfant et ensuite de s'asseoir dans une boucle y "veille" en regardant l'horloge (et éventuellement en surveillant également la sortie de l'enfant).
Peter
44

Voici la solution d'Alex Martelli en tant que module avec destruction de processus appropriée. 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 en lisiez quelque chose.

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 __name__ == '__main__':
    print run('find /', shell = True, timeout = 3)
    print run('find', shell = True)
Björn Lindqvist
la source
3
Cela ne fonctionnera pas sur les fenêtres, et l'ordre des fonctions est inversé.
Hamish Grubijan
3
Cela se traduit parfois par une exception lorsqu'un autre gestionnaire s'enregistre sur SIGALARM et tue le processus avant que celui-ci n'arrive à "tuer", a ajouté une solution de contournement. BTW, grande recette! J'ai utilisé cela pour lancer 50 000 processus de buggy jusqu'à présent sans geler ni planter le wrapper de gestion.
Yaroslav Bulatov
Comment cela peut-il être modifié pour s'exécuter dans une application filetée? J'essaie de l'utiliser à partir des threads de travail et d'obtenirValueError: signal only works in main thread
wim
@Yaroslav Bulatov Merci pour l'info. Quelle solution de contournement avez-vous ajoutée pour résoudre le problème mentionné?
jpswain
1
Juste ajouté le bloc "try; catch", il est dans le code. BTW, à long terme, cela m'a posé des problèmes car vous ne pouvez définir qu'un seul gestionnaire SIGALARM, et d'autres processus peuvent le réinitialiser. Une solution à cela est donnée ici - stackoverflow.com/questions/6553423/…
Yaroslav Bulatov
18

J'ai modifié la réponse de sussudio . Maintenant , la fonction retourne: ( returncode, stdout, stderr, timeout) - stdoutet stderrest décodé à 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"]
Michal Zmuda
la source
18

personne surpris de mentionner en utilisant timeout

timeout 5 ping -c 3 somehost

Cela ne fonctionnera évidemment pas pour chaque cas d'utilisation, mais si vous traitez avec un script simple, c'est difficile à battre.

Également disponible en tant que gtimeout dans coreutils via homebrewpour les utilisateurs de mac.

Karsten
la source
1
vous voulez dire: proc = subprocess.Popen(['/usr/bin/timeout', str(timeout)] + cmd, ...). Existe-t-il une timeoutcommande sous Windows comme le demande OP?
jfs
Dans Windows, on peut utiliser une application comme git bash qui permet les utilitaires bash dans Windows.
Kaushik Acharya
@KaushikAcharya même si vous utilisez git bash, lorsque python appelle un sous-processus, il s'exécutera sur Windows, donc ce contournement ne fonctionnera pas.
Naman Chikara
16

timeoutest désormais pris en charge par call()et communicate()dans le module de sous-processus (à partir de Python3.3):

import subprocess

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

Cela appellera la commande et déclenchera 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.

James
la source
il existe une réponse existante qui mentionne le timeoutparamètre . Bien que le mentionner une fois de plus ne ferait pas de mal.
jfs
//, je pense que OP cherche une solution pour l'ancien Python.
Nathan Basanese
11

Une autre option consiste à écrire dans un fichier temporaire pour éviter 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 les fenêtres.

    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()
Mat
la source
Semble incomplet - qu'est-ce que le fichier temporaire?
spiderplant0
Inclure "import tempfile", "import time" et "shell = True" dans l'appel "Popen" (attention à "shell = True")!
Eduardo Lucio
11

Je ne sais pas pourquoi cela n'est pas mentionné mais depuis Python 3.5, il y a une nouvelle subprocess.runcommande universelle (qui est destinée à remplacer check_call, check_output...) et qui a aussi le timeoutparamètre.

subprocess.run (args, *, stdin = None, input = None, stdout = None, stderr = None, shell = False, cwd = None, timeout = None, check = False, encoding = None, errors = None)

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

Il déclenche une subprocess.TimeoutExpiredexception lorsque le délai est expiré.

Jean-François Fabre
la source
6

Voici ma solution, j'utilisais Thread et 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)
rsk
la source
5

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

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

bortzmeyer
la source
J'utilise un outil similaire appelé timeout- packages.ubuntu.com/search?keywords=timeout - mais aucun ne fonctionne sous Windows, n'est-ce pas?
Sridhar Ratnakumar,
5

J'ai ajouté la solution avec le threading 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
ponty
la source
Le module easyprocess ( code.activestate.com/pypm/easyprocess ) a fonctionné pour moi, même en l'utilisant à partir du multitraitement.
iChux
5

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
ThOong Ku
la source
1
Ne fonctionne probablement pas sur Windows, comme demandé dans la question initiale
Jean-Francois T.
5

Faire précéder la commande Linux timeoutn'est pas une mauvaise solution de contournement et cela a fonctionné pour moi.

cmd = "timeout 20 "+ cmd
subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
Vikram Hosakote
la source
Comment imprimer les chaînes de sortie lors de l'exécution du sous-processus? - Les messages de sortie sont renvoyés par sous-processus.
Ammad
3

J'ai mis en œuvre ce que j'ai pu recueillir à partir de quelques-uns d'entre eux. Cela fonctionne sous Windows, et comme il s'agit d'un wiki communautaire, je pense que je partagerais également 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 à partir 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()
joslinm
la source
En fait, cela ne fonctionne probablement pas. La terminate()fonction marque un thread comme terminé, mais ne termine pas réellement le thread! Je peux le vérifier dans * nix, mais je n'ai pas d'ordinateur Windows pour tester.
dotancohen
2

Une fois que vous aurez compris toutes les machines en cours d'exécution dans * unix, vous trouverez facilement une solution plus simple:

Considérez cet exemple simple comment rendre la meth communicable () expirable en utilisant select.select () (disponible presque partout sur * nix de nos jours). Cela peut également être écrit avec epoll / poll / kqueue, mais la variante select.select () pourrait être un bon exemple pour vous. Et les limitations majeures de select.select () (vitesse et 1024 max fds) ne sont pas applicables à votre tâche.

Cela fonctionne sous * nix, ne crée pas de threads, n'utilise pas de signaux, peut être lancé à partir de n'importe quel thread (non seulement principal) et assez rapide pour lire 250 Mo / s de données depuis la sortie standard de ma machine (i5 2,3 GHz).

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 grande utilisation de la mémoire. Mais vous pouvez appeler plusieurs fois communic () avec des délais plus courts.

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)
Vadim Fint
la source
2
Cela ne concerne que la moitié Unix du problème.
Spaceghost
2

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
dspeyer
la source
1

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

Heikki Toivonen
la source
Il semble que killableprocess n'ajoute pas de délai d'expiration à l'appel Popen.communicate ().
Wim Coenen
1

Bien que je ne l'ai pas beaucoup examiné, ce décorateur que j'ai trouvé à ActiveState semble être très utile pour ce genre de chose. Parallèlement subprocess.Popen(..., close_fds=True), au moins je suis prêt pour l'écriture de scripts shell en Python.

Ehtesh Choudhury
la source
Ce décorateur utilise signal.alarm, qui n'est pas disponible sur Windows.
dbn
1

Cette solution tue l'arborescence des processus en cas de shell = True, transmet les paramètres au processus (ou non), a un délai d'attente et obtient la sortie stdout, stderr et process du rappel (elle utilise psutil pour le kill_proc_tree). Cela était basé sur plusieurs solutions publiées dans SO, y compris celles de 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 remplacer l'appel parent.children (...) par 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()
Tomas
la source
1

Il y a une idée de sous-classer la classe Popen et de l'étendre avec des décorateurs de méthode simples. Appelons-le 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 __name__ == '__main__':
    from subprocess import PIPE

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

J'ai eu le problème que je voulais mettre fin à un sous-processus multithreading si cela prenait plus de temps qu'une durée d'expiration donnée. Je voulais définir un délai d'attente Popen(), mais cela n'a pas fonctionné. Ensuite, j'ai réalisé que Popen().wait()c'est égal à call()et j'ai donc eu l'idée de définir un timeout dans la .wait(timeout=xxx)méthode, 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 __name__ == '__main__':
    main()
Sjor
la source
0

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

Patrick Narkinsky
la source
0

https://pypi.python.org/pypi/python-subprocess2 fournit des extensions au module de sous-processus qui vous permettent d'attendre jusqu'à un certain laps de temps, sinon de terminer.

Donc, pour attendre jusqu'à 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. "results" est un dictionnaire, il contient "returnCode" qui est le retour de l'application (ou None si elle devait être supprimée), ainsi que "actionTaken". qui sera "SUBPROCESS2_PROCESS_COMPLETED" si le processus s'est terminé normalement, ou un masque de "SUBPROCESS2_PROCESS_TERMINATED" et SUBPROCESS2_PROCESS_KILLED selon l'action entreprise (voir la documentation pour plus de détails)

Tim Savannah
la source
0

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
whi
la source
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
Joy Wang
la source
-1
import subprocess, optparse, os, sys, re, datetime, threading, time, glob, shutil, xml.dom.minidom, traceback

class OutputManager:
    def __init__(self, filename, mode, console, logonly):
        self.con = console
        self.logtoconsole = True
        self.logtofile = False

        if filename:
            try:
                self.f = open(filename, mode)
                self.logtofile = True
                if logonly == True:
                    self.logtoconsole = False
            except IOError:
                print (sys.exc_value)
                print ("Switching to console only output...\n")
                self.logtofile = False
                self.logtoconsole = True

    def write(self, data):
        if self.logtoconsole == True:
            self.con.write(data)
        if self.logtofile == True:
            self.f.write(data)
        sys.stdout.flush()

def getTimeString():
        return time.strftime("%Y-%m-%d", time.gmtime())

def runCommand(command):
    '''
    Execute a command in new thread and return the
    stdout and stderr content of it.
    '''
    try:
        Output = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0]
    except Exception as e:
        print ("runCommand failed :%s" % (command))
        print (str(e))
        sys.stdout.flush()
        return None
    return Output

def GetOs():
    Os = ""
    if sys.platform.startswith('win32'):
        Os = "win"
    elif sys.platform.startswith('linux'):
        Os = "linux"
    elif sys.platform.startswith('darwin'):
        Os = "mac"
    return Os


def check_output(*popenargs, **kwargs):
    try:
        if 'stdout' in kwargs: 
            raise ValueError('stdout argument not allowed, it will be overridden.') 

        # Get start time.
        startTime = datetime.datetime.now()
        timeoutValue=3600

        cmd = popenargs[0]

        if sys.platform.startswith('win32'):
            process = subprocess.Popen( cmd, stdout=subprocess.PIPE, shell=True) 
        elif sys.platform.startswith('linux'):
            process = subprocess.Popen( cmd , stdout=subprocess.PIPE, shell=True ) 
        elif sys.platform.startswith('darwin'):
            process = subprocess.Popen( cmd , stdout=subprocess.PIPE, shell=True ) 

        stdoutdata, stderrdata = process.communicate( timeout = timeoutValue )
        retcode = process.poll()

        ####################################
        # Catch crash error and log it.
        ####################################
        OutputHandle = None
        try:
            if retcode >= 1:
                OutputHandle = OutputManager( 'CrashJob_' + getTimeString() + '.txt', 'a+', sys.stdout, False)
                OutputHandle.write( cmd )
                print (stdoutdata)
                print (stderrdata)
                sys.stdout.flush()
        except Exception as e:
            print (str(e))

    except subprocess.TimeoutExpired:
            ####################################
            # Catch time out error and log it.
            ####################################
            Os = GetOs()
            if Os == 'win':
                killCmd = "taskkill /FI \"IMAGENAME eq {0}\" /T /F"
            elif Os == 'linux':
                killCmd = "pkill {0)"
            elif Os == 'mac':
                # Linux, Mac OS
                killCmd = "killall -KILL {0}"

            runCommand(killCmd.format("java"))
            runCommand(killCmd.format("YouApp"))

            OutputHandle = None
            try:
                OutputHandle = OutputManager( 'KillJob_' + getTimeString() + '.txt', 'a+', sys.stdout, False)
                OutputHandle.write( cmd )
            except Exception as e:
                print (str(e))
    except Exception as e:
            for frame in traceback.extract_tb(sys.exc_info()[2]):
                        fname,lineno,fn,text = frame
                        print "Error in %s on line %d" % (fname, lineno)
PeterZhao
la source
c'est une abomination
Corey Goldberg
-2

J'essayais juste d'écrire quelque chose de plus simple.

#!/usr/bin/python

from subprocess import Popen, PIPE
import datetime
import time 

popen = Popen(["/bin/sleep", "10"]);
pid = popen.pid
sttime = time.time();
waittime =  3

print "Start time %s"%(sttime)

while True:
    popen.poll();
    time.sleep(1)
    rcode = popen.returncode
    now = time.time();
    if [ rcode is None ]  and  [ now > (sttime + waittime) ] :
        print "Killing it now"
        popen.kill()
Jabir Ahmed
la source
time.sleep (1) est une très mauvaise idée - imaginez que vous souhaitiez exécuter de nombreuses commandes qui prendraient environ 0,002 s. Vous devriez plutôt attendre pendant que poll () (voir select, pour Linux epol recommandé :)
ddzialak