Wie kann ich einen externen Befehl asynchron in Python ausführen?

118

Ich muss einen Shell-Befehl asynchron über ein Python-Skript ausführen. Damit meine ich, dass mein Python-Skript weiterhin ausgeführt werden soll, während der externe Befehl ausgeht und alles tut, was er tun muss.

Ich habe diesen Beitrag gelesen:

Aufrufen eines externen Befehls in Python

Ich habe dann einige Tests durchgeführt, und es sieht so aus, os.system()als würde ich den Job erledigen, vorausgesetzt, ich verwende ihn &am Ende des Befehls, damit ich nicht warten muss, bis er zurückkommt. Ich frage mich, ob dies der richtige Weg ist, um so etwas zu erreichen. Ich habe es versucht, commands.call()aber es wird bei mir nicht funktionieren, da es den externen Befehl blockiert.

Bitte lassen Sie mich wissen, ob eine Verwendung os.system()hierfür ratsam ist oder ob ich eine andere Route ausprobieren sollte.

Gemeinschaft
quelle

Antworten:

133

subprocess.Popen macht genau das, was Sie wollen.

from subprocess import Popen
p = Popen(['watch', 'ls']) # something long running
# ... do other stuff while subprocess is running
p.terminate()

(Bearbeiten, um die Antwort aus den Kommentaren zu vervollständigen)

Die Popen-Instanz kann verschiedene andere Aktionen ausführen, um festzustellen, poll()ob sie noch ausgeführt wird, und Sie können communicate()damit Daten auf stdin senden und warten, bis sie beendet wird.

Ali Afshar
quelle
4
Sie können auch poll () verwenden, um zu überprüfen, ob der untergeordnete Prozess beendet wurde, oder wait () verwenden, um auf das Beenden zu warten.
Adam Rosenfield
Adam, sehr wahr, obwohl es besser sein könnte, communic () zum Warten zu verwenden, da dadurch die In / Out-Puffer besser gehandhabt werden und es Situationen gibt, in denen diese überflutet werden könnten.
Ali Afshar
Adam: In den Dokumenten heißt es: "Warnung Dies führt zu einem Deadlock, wenn der untergeordnete Prozess genügend Ausgabe für eine stdout- oder stderr-Pipe generiert, sodass das Warten auf die Aufnahme weiterer Daten durch den OS-Pipe-Puffer blockiert wird. Verwenden Sie communic (), um dies zu vermeiden."
Ali Afshar
14
communic () und wait () blockieren jedoch Vorgänge. Sie werden keine Befehle parallelisieren, wie das OP zu fragen scheint, ob Sie sie verwenden.
cdleary
1
Cdleary ist absolut korrekt. Es sollte erwähnt werden, dass Kommunikation und Warten blockiert werden. Tun Sie dies also nur, wenn Sie darauf warten, dass die Dinge heruntergefahren werden. (Was Sie wirklich tun sollten, um sich gut zu benehmen)
Ali Afshar
48

Wenn Sie viele Prozesse parallel ausführen und dann verarbeiten möchten, wenn sie zu Ergebnissen führen, können Sie die Abfrage wie folgt verwenden:

from subprocess import Popen, PIPE
import time

running_procs = [
    Popen(['/usr/bin/my_cmd', '-i %s' % path], stdout=PIPE, stderr=PIPE)
    for path in '/tmp/file0 /tmp/file1 /tmp/file2'.split()]

while running_procs:
    for proc in running_procs:
        retcode = proc.poll()
        if retcode is not None: # Process finished.
            running_procs.remove(proc)
            break
        else: # No process is done, wait a bit and check again.
            time.sleep(.1)
            continue

    # Here, `proc` has finished with return code `retcode`
    if retcode != 0:
        """Error handling."""
    handle_results(proc.stdout)

Der Kontrollfluss dort ist etwas kompliziert, weil ich versuche, ihn klein zu machen - Sie können ihn nach Ihrem Geschmack umgestalten. :-)

Dies hat den Vorteil, dass die Anforderungen für die frühzeitige Fertigstellung zuerst bearbeitet werden. Wenn Sie communicateden ersten laufenden Prozess aufrufen und sich herausstellt, dass dieser am längsten läuft, haben die anderen laufenden Prozesse dort im Leerlauf gesessen, als Sie ihre Ergebnisse hätten verarbeiten können.

cdleary
quelle
3
@Tino Es hängt davon ab, wie Sie Busy-Wait definieren. Siehe Was ist der Unterschied zwischen Besetzt-Warten und Abrufen?
Piotr Dobrogost
1
Gibt es eine Möglichkeit, eine Reihe von Prozessen abzufragen, nicht nur einen?
Piotr Dobrogost
1
Hinweis: Es kann hängen bleiben, wenn ein Prozess genügend Ausgabe generiert. Sie sollten stdout gleichzeitig verwenden, wenn Sie PIPE verwenden (es gibt (zu viele, aber nicht genug) Warnungen in den Dokumenten des Unterprozesses darüber).
JFS
@PiotrDobrogost: Sie können os.waitpiddirekt verwenden, um zu überprüfen, ob ein untergeordneter Prozess seinen Status geändert hat.
JFS
5
Verwenden Sie ['/usr/bin/my_cmd', '-i', path]anstelle von['/usr/bin/my_cmd', '-i %s' % path]
jfs
11

Ich frage mich, ob dieses [os.system ()] der richtige Weg ist, um so etwas zu erreichen.

Nein os.system()ist nicht der richtige Weg. Deshalb sagt jeder zu verwenden subprocess.

Weitere Informationen finden Sie unter http://docs.python.org/library/os.html#os.system

Das Unterprozessmodul bietet leistungsfähigere Funktionen zum Laichen neuer Prozesse und zum Abrufen ihrer Ergebnisse. Die Verwendung dieses Moduls ist der Verwendung dieser Funktion vorzuziehen. Verwenden Sie das Unterprozessmodul. Überprüfen Sie insbesondere den Abschnitt Ersetzen älterer Funktionen durch den Abschnitt Unterprozessmodul.

S.Lott
quelle
8

Ich habe gute Erfolge mit dem asyncproc- Modul erzielt , das sich gut mit der Ausgabe der Prozesse befasst. Beispielsweise:

import os
from asynproc import Process
myProc = Process("myprogram.app")

while True:
    # check to see if process has ended
    poll = myProc.wait(os.WNOHANG)
    if poll is not None:
        break
    # print any new output
    out = myProc.read()
    if out != "":
        print out
Noah
quelle
ist das irgendwo auf Github?
Nick
Es ist eine gpl-Lizenz, also bin ich mir sicher, dass sie viele Male dort ist. Hier ist eine: github.com/albertz/helpers/blob/master/asyncproc.py
Noah
Ich habe einen Kern mit einigen Modifikationen hinzugefügt, damit es mit Python3 funktioniert. (Ersetzt meistens die str durch Bytes). Siehe gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic
1
Außerdem müssen Sie die Ausgabe nach dem Verlassen der Schleife noch einmal lesen, da Sie sonst einen Teil der Ausgabe verlieren.
Tic
7

Die Verwendung von pexpect mit nicht blockierenden Readlines ist eine weitere Möglichkeit, dies zu tun. Pexpect löst die Deadlock-Probleme, ermöglicht das einfache Ausführen der Prozesse im Hintergrund und bietet einfache Möglichkeiten für Rückrufe, wenn Ihr Prozess vordefinierte Zeichenfolgen ausspuckt, und erleichtert im Allgemeinen die Interaktion mit dem Prozess erheblich.

Gabe
quelle
4

In Anbetracht von "Ich muss nicht warten, bis es zurückkommt" ist eine der einfachsten Lösungen folgende:

subprocess.Popen( \
    [path_to_executable, arg1, arg2, ... argN],
    creationflags = subprocess.CREATE_NEW_CONSOLE,
).pid

Aber ... Nach dem, was ich gelesen habe, ist dies aufgrund der Sicherheitsrisiken von nicht "der richtige Weg, um so etwas zu erreichen" subprocess.CREATE_NEW_CONSOLE Flagge verursachten .

Die wichtigsten Dinge, die hier passieren, sind die subprocess.CREATE_NEW_CONSOLEErstellung einer neuen Konsole und .pid(gibt die Prozess-ID zurück, damit Sie das Programm später überprüfen können, wenn Sie möchten), damit Sie nicht warten müssen, bis das Programm seinen Job beendet hat.

Pugsley
quelle
3

Ich habe das gleiche Problem beim Versuch, mit der S3270-Skriptsoftware in Python eine Verbindung zu einem 3270-Terminal herzustellen. Jetzt löse ich das Problem mit einer Unterklasse von Process, die ich hier gefunden habe:

http://code.activestate.com/recipes/440554/

Und hier ist das Beispiel aus der Datei:

def recv_some(p, t=.1, e=1, tr=5, stderr=0):
    if tr < 1:
        tr = 1
    x = time.time()+t
    y = []
    r = ''
    pr = p.recv
    if stderr:
        pr = p.recv_err
    while time.time() < x or r:
        r = pr()
        if r is None:
            if e:
                raise Exception(message)
            else:
                break
        elif r:
            y.append(r)
        else:
            time.sleep(max((x-time.time())/tr, 0))
    return ''.join(y)

def send_all(p, data):
    while len(data):
        sent = p.send(data)
        if sent is None:
            raise Exception(message)
        data = buffer(data, sent)

if __name__ == '__main__':
    if sys.platform == 'win32':
        shell, commands, tail = ('cmd', ('dir /w', 'echo HELLO WORLD'), '\r\n')
    else:
        shell, commands, tail = ('sh', ('ls', 'echo HELLO WORLD'), '\n')

    a = Popen(shell, stdin=PIPE, stdout=PIPE)
    print recv_some(a),
    for cmd in commands:
        send_all(a, cmd + tail)
        print recv_some(a),
    send_all(a, 'exit' + tail)
    print recv_some(a, e=0)
    a.wait()
Patrizio Rullo
quelle
2

Die akzeptierte Antwort ist sehr alt.

Ich habe hier eine bessere moderne Antwort gefunden:

https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/

und einige Änderungen vorgenommen:

  1. Lass es unter Windows funktionieren
  2. Lass es mit mehreren Befehlen funktionieren
import sys
import asyncio

if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())


async def _read_stream(stream, cb):
    while True:
        line = await stream.readline()
        if line:
            cb(line)
        else:
            break


async def _stream_subprocess(cmd, stdout_cb, stderr_cb):
    try:
        process = await asyncio.create_subprocess_exec(
            *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
        )

        await asyncio.wait(
            [
                _read_stream(process.stdout, stdout_cb),
                _read_stream(process.stderr, stderr_cb),
            ]
        )
        rc = await process.wait()
        return process.pid, rc
    except OSError as e:
        # the program will hang if we let any exception propagate
        return e


def execute(*aws):
    """ run the given coroutines in an asyncio loop
    returns a list containing the values returned from each coroutine.
    """
    loop = asyncio.get_event_loop()
    rc = loop.run_until_complete(asyncio.gather(*aws))
    loop.close()
    return rc


def printer(label):
    def pr(*args, **kw):
        print(label, *args, **kw)

    return pr


def name_it(start=0, template="s{}"):
    """a simple generator for task names
    """
    while True:
        yield template.format(start)
        start += 1


def runners(cmds):
    """
    cmds is a list of commands to excecute as subprocesses
    each item is a list appropriate for use by subprocess.call
    """
    next_name = name_it().__next__
    for cmd in cmds:
        name = next_name()
        out = printer(f"{name}.stdout")
        err = printer(f"{name}.stderr")
        yield _stream_subprocess(cmd, out, err)


if __name__ == "__main__":
    cmds = (
        [
            "sh",
            "-c",
            """echo "$SHELL"-stdout && sleep 1 && echo stderr 1>&2 && sleep 1 && echo done""",
        ],
        [
            "bash",
            "-c",
            "echo 'hello, Dave.' && sleep 1 && echo dave_err 1>&2 && sleep 1 && echo done",
        ],
        [sys.executable, "-c", 'print("hello from python");import sys;sys.exit(2)'],
    )

    print(execute(*runners(cmds)))

Es ist unwahrscheinlich, dass die Beispielbefehle auf Ihrem System einwandfrei funktionieren, und es werden keine seltsamen Fehler behandelt. Dieser Code zeigt jedoch eine Möglichkeit, mehrere Unterprozesse mit Asyncio auszuführen und die Ausgabe zu streamen.

Terrel Shumway
quelle
Ich habe dies auf cpython 3.7.4 unter Windows und cpython 3.7.3 unter Ubuntu WSL und nativem Alpine Linux getestet
Terrel Shumway
1

Hier gibt es mehrere Antworten, aber keine davon hat meine folgenden Anforderungen erfüllt:

  1. Ich möchte nicht warten, bis der Befehl beendet ist, oder mein Terminal mit Unterprozessausgaben verschmutzen.

  2. Ich möchte ein Bash-Skript mit Weiterleitungen ausführen.

  3. Ich möchte Piping in meinem Bash-Skript unterstützen (zum Beispiel find ... | tar ...).

Die einzige Kombination, die die oben genannten Anforderungen erfüllt, ist:

subprocess.Popen(['./my_script.sh "arg1" > "redirect/path/to"'],
                 stdout=subprocess.PIPE, 
                 stderr=subprocess.PIPE,
                 shell=True)
Shital Shah
quelle
0

Dies wird in Python 3-Unterprozessbeispielen unter "Warten auf asynchrones Beenden des Befehls" behandelt:

import asyncio

proc = await asyncio.create_subprocess_exec(
    'ls','-lha',
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.PIPE)

# do something else while ls is working

# if proc takes very long to complete, the CPUs are free to use cycles for 
# other processes
stdout, stderr = await proc.communicate()

Der Prozess wird gestartet, sobald der Vorgang await asyncio.create_subprocess_exec(...)abgeschlossen ist. Wenn es zum Zeitpunkt await proc.communicate()Ihres Anrufs noch nicht fertig ist , wartet es dort, um Ihnen Ihren Ausgabestatus mitzuteilen. Wenn es fertig ist,proc.communicate() wird sofort zurückkehren.

Das Wesentliche hier ist ähnlich wie bei Terrels Antwort aber ich denke, die Antwort von Terrels scheint die Dinge .

Siehe asyncio.create_subprocess_execfür weitere Informationen.

gerrit
quelle