wake-up-neo.com

Modul 'Unterprozess' mit Timeout verwenden

Hier ist der Python-Code, um einen beliebigen Befehl auszuführen, der seine stdout-Daten zurückgibt, oder eine Exit-Ausnahme für Exit-Codes ungleich Null auszulösen:

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

communicate wird verwendet, um zu warten, bis der Prozess beendet wird:

stdoutdata, stderrdata = proc.communicate()

Das Modul subprocess unterstützt kein Zeitlimit - die Möglichkeit, einen Prozess abzubrechen, der länger als X Sekunden ausgeführt wird - daher kann es dauern, dass communicate ausgeführt wird.

Was ist der simplest Weg, um Timeouts in einem Python-Programm zu implementieren, das unter Windows und Linux ausgeführt wird?

274

In Python 3.3+:

from subprocess import STDOUT, check_output

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

output ist eine Bytezeichenfolge, die die zusammengeführten stdout-, stderr-Daten des Befehls enthält. 

Dieser Code löst CalledProcessError für den Exit-Status ungleich Null aus, wie im Text der Frage angegeben, im Gegensatz zur proc.communicate()-Methode.

Ich habe Shell=True entfernt, da er oft unnötig verwendet wird. Sie können es immer wieder hinzufügen, wenn cmd es tatsächlich erfordert. Wenn Sie Shell=True hinzufügen, d. H., Wenn der untergeordnete Prozess seine eigenen Nachkommen erzeugt; check_output() kann viel später zurückkehren, als das Timeout angibt, siehe Fehler beim Subprozess-Timeout .

Die Timeout-Funktion ist in Python 2.x über den subprocess32 backport des 3.2 + -Unterverarbeitungsmoduls verfügbar.

135
jfs

Ich weiß nicht viel über die Details auf niedriger Ebene. In Anbetracht dessen, dass in python 2.6 die API die Möglichkeit bietet, auf Threads zu warten und Prozesse zu beenden, wie sieht es damit aus, den Prozess in einem separaten Thread auszuführen?

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)

Die Ausgabe dieses Snippets in meiner Maschine lautet:

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

dabei ist zu sehen, dass der Prozess in der ersten Ausführung korrekt abgeschlossen wurde (Rückkehrcode 0), während in der zweiten Ausführung der Prozess beendet wurde (Rückkehrcode -15).

Ich habe nicht in Fenstern getestet. Abgesehen von der Aktualisierung des Beispielbefehls sollte dies jedoch funktionieren, da ich in der Dokumentation nicht gefunden habe, dass thread.join oder process.terminate nicht unterstützt wird.

193
jcollado

die Antwort von jcollado kann mit der threading.Timer class vereinfacht werden:

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

Wenn Sie auf Unix sind,

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

Hier ist die Lösung von Alex Martelli als Modul mit ordnungsgemäßem Prozessabbruch. Die anderen Ansätze funktionieren nicht, da sie proc.communicate () nicht verwenden. Wenn Sie also einen Prozess haben, der viele Ausgaben erzeugt, füllt er den Ausgabepuffer und blockiert ihn, bis Sie etwas davon lesen.

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

Ich habe sussudio answer geändert. Nun gibt die Funktion zurück: (returncode, stdout, stderr, timeout) - stdout und stderr werden in utf-8-String dekodiert

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

Überrascht hat niemand erwähnt, dass timeout

timeout 5 ping -c 3 somehost

Dies gilt natürlich nicht für jeden Anwendungsfall, aber wenn Sie mit einem einfachen Skript umgehen, ist dies schwer zu schlagen.

Auch als Gtimeout in coreutils über homebrew für Mac-Benutzer verfügbar.

15
Karsten

Eine andere Möglichkeit ist, in eine temporäre Datei zu schreiben, um das Blockieren des stdout zu verhindern, anstatt mit communic () abzufragen. Dies funktionierte für mich, wo die anderen Antworten nicht waren; zum Beispiel an Fenstern.

    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 wird jetzt unterstützt von call() und communicate() im Unterprozess-Modul (ab Python3.3):

import subprocess

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

Dadurch wird der Befehl aufgerufen und die Ausnahme ausgelöst

subprocess.TimeoutExpired

wenn der Befehl nach 20 Sekunden nicht beendet wird.

Sie können dann mit der Ausnahme umgehen, um Ihren Code fortzusetzen, etwa:

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

Hoffe das hilft.

8
James

Hier ist meine Lösung, ich habe Thread und Event verwendet:

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

In Aktion:

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

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

Die Lösung, die ich verwende, besteht darin, dem Shell-Befehl timelimit voranzustellen. Wenn der Befehl zu lange dauert, stoppt das Zeitlimit, und Popen wird einen Rückgabecode per Zeitlimit festlegen. Wenn es> 128 ist, bedeutet das, dass der Prozess durch Zeitlimit beendet wurde.

Siehe auch python-Subprozess mit Timeout und großer Ausgabe (> 64 KB)

4
bortzmeyer

Ich habe die Lösung mit Threading von jcollado zu meinem Python-Modul easyprocess hinzugefügt.

Installieren:

pip install easyprocess

Beispiel:

from easyprocess import Proc

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

wenn Sie Python 2 verwenden, probieren Sie es aus

import subprocess32

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

Ich weiß nicht, warum es nicht erwähnt wird, aber seit Python 3.5 gibt es einen neuen subprocess.run universal-Befehl (der check_call, check_output ... ersetzen soll) und der ebenfalls den timeout-Parameter hat.

subprocess.run (args, *, stdin = keine, Eingabe = keine, stdout = keine, stderr = keine, Shell = falsch, cwd = keine, timeout = keine, check = falsch, Kodierung = keine, Fehler = keine).

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

Es tritt eine subprocess.TimeoutExpired-Ausnahme auf, wenn das Timeout abgelaufen ist.

Wenn Sie mit dem vollständigen Prozess von Maschinen unter * Unix vertraut sind, werden Sie leicht eine einfachere Lösung finden:

Betrachten Sie dieses einfache Beispiel, wie timeoutable () meth mit select.select () (derzeit fast überall auf * nix verfügbar) gemacht wird. Dies kann auch mit epoll/poll/kqueue geschrieben werden, aber die select.select () - Variante könnte für Sie ein gutes Beispiel sein. Die wichtigsten Einschränkungen von select.select () (Geschwindigkeit und maximal 1024 fds) sind für Ihre Aufgabe nicht anwendbar.

Dies funktioniert unter * nix, erstellt keine Threads, verwendet keine Signale, kann von jedem Thread (nicht nur Main) aus gestartet werden, und es ist schnell genug, um 250 MBit/s an Daten von stdout auf meinem Computer zu lesen (i5 2,3 GHz).

Es gibt ein Problem, wenn Sie stdout/stderr am Ende der Kommunikation beitreten. Wenn Sie große Programmausgaben haben, kann dies zu einer hohen Speicherauslastung führen. Sie können jedoch communic () mehrmals mit kleineren Timeouts aufrufen.

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

Das Vorbereiten des Linux-Befehls timeout ist keine schlechte Umgehung und es hat für mich funktioniert.

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

Sie können dies mit select tun

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

Ich habe umgesetzt, was ich aus ein paar davon entnehmen konnte. Dies funktioniert in Windows, und da dies ein Community-Wiki ist, würde ich meinen Code ebenfalls teilen:

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

Dann von einer anderen Klasse oder Datei:

        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

Obwohl ich es nicht ausführlich betrachtet habe, scheint dieser decorator , den ich bei ActiveState gefunden habe, für diese Art von Sache sehr nützlich zu sein. Zusammen mit subprocess.Popen(..., close_fds=True) bin ich zumindest bereit für Shell-Scripting in Python.

1

Ich hatte das Problem, dass ich einen Multithreading-Subprozess beenden wollte, wenn er länger als eine bestimmte Timeout-Zeit dauerte. Ich wollte ein Timeout in Popen() setzen, aber es hat nicht funktioniert. Dann wurde mir klar, dass Popen().wait() gleich call() ist, und so kam ich auf die Idee, ein Timeout innerhalb der .wait(timeout=xxx)-Methode festzulegen, was schließlich funktioniert hat. So habe ich es so gelöst:

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

Es gibt eine Idee, die Popen-Klasse zu unterteilen und um einige einfache Methodendekorateure zu erweitern. Nennen wir es 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

Ich habe killableprocess erfolgreich unter Windows, Linux und Mac verwendet. Wenn Sie Cygwin Python verwenden, benötigen Sie OSAFs Version von killableprocess , da sonst native Windows-Prozesse nicht beendet werden.

1
Heikki Toivonen

Diese Lösung beendet den Prozessbaum bei Shell = True, übergibt Parameter an den Prozess (oder nicht), hat eine Zeitüberschreitung und ruft die Ausgabe von stdout, stderr und process des Rückrufs ab (er verwendet psutil für kill_proc_tree). Dies basierte auf mehreren in SO veröffentlichten Lösungen, einschließlich jcollados. Antwort als Antwort auf die Kommentare von Anson und jradice in der Antwort von jcollado. Getestet in Windows Srvr 2012 und Ubuntu 14.04. Bitte beachten Sie, dass Sie für Ubuntu den Aufruf von parent.children (...) in parent.get_children (...) ändern müssen.

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

verwenden Sie für Python 2.6+ 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 bietet Erweiterungen für das Unterprozess-Modul, mit denen Sie bis zu einer bestimmten Zeit warten können, andernfalls beenden Sie das Programm. 

So warten Sie bis zu 10 Sekunden, bis der Prozess beendet ist, andernfalls töten Sie Folgendes:

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

timeout =  10

results = pipe.waitOrTerminate(timeout)

Dies ist sowohl mit Windows als auch mit Unix kompatibel. "results" ist ein Wörterbuch. Es enthält "returnCode", dh die Rückgabe der App (oder None, wenn sie beendet werden musste) sowie "actionTaken". Dies ist "SUBPROCESS2_PROCESS_COMPLETED", wenn der Vorgang normal abgeschlossen wurde, oder eine Maske aus "SUBPROCESS2_PROCESS_TERMINATED" und SUBPROCESS2_PROCESS_KILLED (abhängig von der durchgeführten Aktion).

0
Tim Savannah

Leider bin ich an sehr strenge Richtlinien bezüglich der Offenlegung des Quellcodes durch meinen Arbeitgeber gebunden, so dass ich keinen tatsächlichen Code angeben kann. Für meinen Geschmack ist es jedoch die beste Lösung, eine Unterklasse zu erstellen, die Popen.wait() überschreibt, um abzufragen, anstatt unendlich lange zu warten, und Popen.__init__, um einen Timeout-Parameter zu akzeptieren. Wenn Sie dies getan haben, funktionieren alle anderen Popen-Methoden (die wait aufrufen) erwartungsgemäß, einschließlich 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