Comment puis-je exécuter une commande externe de manière asynchrone à partir de Python?

120

J'ai besoin d'exécuter une commande shell de manière asynchrone à partir d'un script Python. Je veux dire par là que je veux que mon script Python continue à s'exécuter pendant que la commande externe se déclenche et fait tout ce qu'elle doit faire.

J'ai lu cet article:

Appeler une commande externe en Python

Je suis ensuite parti et j'ai fait des tests, et il semble que os.system()je ferai le travail à condition que j'utilise &à la fin de la commande afin de ne pas avoir à attendre son retour. Ce que je me demande, c'est si c'est la bonne façon d'accomplir une telle chose? J'ai essayé commands.call()mais cela ne fonctionnera pas pour moi car il bloque sur la commande externe.

S'il vous plaît laissez-moi savoir si utiliser os.system()pour cela est conseillé ou si je devrais essayer une autre voie.

Communauté
la source

Réponses:

135

subprocess.Popen fait exactement ce que vous voulez.

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

(Modifier pour compléter la réponse des commentaires)

L'instance Popen peut faire diverses autres choses comme vous pouvez poll()elle pour voir si elle est toujours en cours d'exécution, et vous pouvez communicate()avec elle pour lui envoyer des données sur stdin et attendre qu'elle se termine.

Ali Afshar
la source
4
Vous pouvez également utiliser poll () pour vérifier si le processus enfant s'est terminé ou utiliser wait () pour attendre qu'il se termine.
Adam Rosenfield
Adam, très vrai, bien qu'il puisse être préférable d'utiliser communique () pour attendre car cela a une meilleure gestion des tampons d'entrée / sortie et il y a des situations où l'inondation de ceux-ci pourrait bloquer.
Ali Afshar
Adam: les documents disent: "Attention, cela bloquera si le processus enfant génère suffisamment de sortie vers un tube stdout ou stderr pour qu'il bloque l'attente que le tampon du tube du système d'exploitation accepte plus de données. Utilisez commun () pour éviter cela."
Ali Afshar
14
communique () et wait () bloquent cependant les opérations. Vous ne serez pas paralléliser les commandes comme l'OP semble vous demander si vous les utilisez.
cdleary
1
Cdleary est tout à fait correct, il convient de mentionner que la communication et l'attente bloquent, alors ne le faites que lorsque vous attendez que les choses s'arrêtent. (Ce que vous devriez vraiment faire pour être bien élevé)
Ali Afshar
48

Si vous souhaitez exécuter de nombreux processus en parallèle, puis les gérer lorsqu'ils donnent des résultats, vous pouvez utiliser l'interrogation comme ci-dessous:

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)

Le flux de contrôle y est un peu compliqué parce que j'essaie de le rendre petit - vous pouvez le refactoriser à votre goût. :-)

Cela présente l'avantage de répondre en premier aux demandes de finition anticipée. Si vous appelez communicatele premier processus en cours d'exécution et que celui-ci s'exécute le plus longtemps, les autres processus en cours d'exécution seront restés inactifs alors que vous auriez pu gérer leurs résultats.

cdleary
la source
3
@Tino Cela dépend de la façon dont vous définissez l'attente occupée. Voir Quelle est la différence entre occupé-wait et polling?
Piotr Dobrogost
1
Existe-t-il un moyen d'interroger un ensemble de processus et non un seul?
Piotr Dobrogost
1
Remarque: il peut se bloquer si un processus génère suffisamment de sortie. Vous devriez consommer stdout simultanément si vous utilisez PIPE (il y a (trop mais pas assez) d'avertissements dans la documentation du sous-processus à ce sujet).
jfs
@PiotrDobrogost: vous pouvez utiliser os.waitpiddirectement ce qui permet de vérifier si un processus enfant a changé son statut.
jfs
5
utiliser à la ['/usr/bin/my_cmd', '-i', path]place de['/usr/bin/my_cmd', '-i %s' % path]
jfs
11

Ce que je me demande, c'est si ce [os.system ()] est la bonne façon d'accomplir une telle chose?

Non, ce os.system()n'est pas la bonne manière. C'est pourquoi tout le monde dit d'utiliser subprocess.

Pour plus d'informations, lisez http://docs.python.org/library/os.html#os.system

Le module de sous-processus fournit des fonctionnalités plus puissantes pour générer de nouveaux processus et récupérer leurs résultats; l'utilisation de ce module est préférable à l'utilisation de cette fonction. Utilisez le module de sous-processus. Vérifiez en particulier la section Remplacement d'anciennes fonctions par la section Module de sous-processus.

S.Lott
la source
8

J'ai eu un bon succès avec le module asyncproc , qui traite bien la sortie des processus. Par exemple:

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
Noé
la source
est-ce quelque part sur github?
Nick
C'est une licence gpl, donc je suis sûr que c'est là-bas plusieurs fois. En voici une: github.com/albertz/helpers/blob/master/asyncproc.py
Noah
J'ai ajouté un résumé avec quelques modifications pour le faire fonctionner avec python3. (remplace principalement la chaîne par des octets). Voir gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic
1
De plus, vous devez lire la sortie une fois de plus après être sorti de la boucle ou vous perdrez une partie de la sortie.
Tic
7

Utiliser pexpect avec des readlines non bloquantes est une autre façon de le faire. Pexpect résout les problèmes de blocage, vous permet d'exécuter facilement les processus en arrière-plan et offre des moyens faciles d'avoir des rappels lorsque votre processus crache des chaînes prédéfinies, et facilite généralement l'interaction avec le processus.

Gabe
la source
4

Considérant "Je n'ai pas à attendre son retour", l'une des solutions les plus simples sera la suivante:

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

Mais ... D'après ce que j'ai lu, ce n'est pas "la bonne façon d'accomplir une telle chose" à cause des risques de sécurité créés par le subprocess.CREATE_NEW_CONSOLEdrapeau.

Les choses clés qui se produisent ici sont l'utilisation de subprocess.CREATE_NEW_CONSOLEpour créer une nouvelle console et .pid(retourne l'ID de processus afin que vous puissiez vérifier le programme plus tard si vous le souhaitez) afin de ne pas attendre que le programme termine son travail.

Pugsley
la source
3

J'ai le même problème en essayant de me connecter à un terminal 3270 en utilisant le logiciel de script s3270 en Python. Maintenant, je résous le problème avec une sous-classe de Processus que j'ai trouvée ici:

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

Et voici l'exemple extrait du fichier:

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
la source
3

La réponse acceptée est très ancienne.

J'ai trouvé une meilleure réponse moderne ici:

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

et apporté quelques modifications:

  1. le faire fonctionner sous windows
  2. le faire fonctionner avec plusieurs commandes
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)))

Il est peu probable que les exemples de commandes fonctionnent parfaitement sur votre système et ne gèrent pas les erreurs étranges, mais ce code montre une façon d'exécuter plusieurs sous-processus en utilisant asyncio et de diffuser la sortie.

Terrel Shumway
la source
J'ai testé cela sur cpython 3.7.4 sous Windows et cpython 3.7.3 sous Ubuntu WSL et Alpine Linux natif
Terrel Shumway
1

Il y a plusieurs réponses ici, mais aucune d'entre elles ne répond aux exigences ci-dessous:

  1. Je ne veux pas attendre que la commande se termine ou polluer mon terminal avec des sorties de sous-processus.

  2. Je veux exécuter un script bash avec des redirections.

  3. Je souhaite prendre en charge la tuyauterie dans mon script bash (par exemple find ... | tar ...).

La seule combinaison qui satisfait aux exigences ci-dessus est:

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

Ceci est couvert par les exemples de sous- processus Python 3 sous "Attendre que la commande se termine de manière asynchrone":

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

Le processus commencera à s'exécuter dès que le await asyncio.create_subprocess_exec(...)sera terminé. S'il n'est pas terminé au moment où vous appelez await proc.communicate(), il y attendra pour vous donner l'état de votre sortie. S'il a terminé, proc.communicate()reviendra immédiatement.

L'essentiel ici est similaire à la réponse de Terrels, mais je pense que la réponse de Terrels semble trop compliquer les choses.

Voir asyncio.create_subprocess_exec pour plus d'informations.

gerrit
la source