¿Cómo puedo ejecutar un comando externo de forma asincrónica desde Python?

120

Necesito ejecutar un comando de shell de forma asincrónica desde un script de Python. Con esto quiero decir que quiero que mi script de Python continúe ejecutándose mientras el comando externo se apaga y hace lo que sea necesario.

Leí esta publicación:

Llamar a un comando externo en Python

Luego me fui e hice algunas pruebas, y parece os.system()que hará el trabajo siempre que use &al final del comando para no tener que esperar a que regrese. Lo que me pregunto es si esta es la forma correcta de lograr tal cosa. Lo intenté commands.call()pero no me funcionará porque bloquea el comando externo.

Por favor, avíseme si os.system()es aconsejable usarlo para esto o si debería probar alguna otra ruta.

Comunidad
fuente

Respuestas:

135

subprocess.Popen hace exactamente lo que quiere.

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

(Edite para completar la respuesta de los comentarios)

La instancia de Popen puede hacer varias otras cosas, como puede hacer poll()para ver si todavía se está ejecutando, y puede communicate()enviarle datos en stdin y esperar a que termine.

Ali Afshar
fuente
4
También puede usar poll () para verificar si el proceso hijo ha terminado, o usar wait () para esperar a que termine.
Adam Rosenfield
Adam, muy cierto, aunque podría ser mejor usar comunica () para esperar porque tiene un mejor manejo de búfer de entrada / salida y hay situaciones en las que la inundación de estos podría bloquear.
Ali Afshar
Adam: los documentos dicen "Advertencia. Esto se bloqueará si el proceso hijo genera suficiente salida en una tubería stdout o stderr de modo que se bloquee a la espera de que el búfer de la tubería del sistema operativo acepte más datos. Use comunica () para evitar eso".
Ali Afshar
14
Sin embargo, comunicar () y esperar () están bloqueando operaciones. No podrá paralelizar comandos como el OP parece preguntar si los usa.
cdleary
1
Cdleary es absolutamente correcto, debe mencionarse que comunicarse y esperar a bloquear, así que solo hágalo cuando esté esperando que las cosas se apaguen. (Lo que realmente deberías hacer para comportarte bien)
Ali Afshar
48

Si desea ejecutar muchos procesos en paralelo y luego manejarlos cuando produzcan resultados, puede usar el sondeo como en el siguiente:

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)

El flujo de control allí es un poco complicado porque estoy tratando de hacerlo pequeño, puede refactorizar a su gusto. :-)

Esto tiene la ventaja de atender primero las solicitudes de finalización anticipada. Si llama communicateal primer proceso en ejecución y resulta que se ejecuta por más tiempo, los otros procesos en ejecución habrán estado inactivos cuando podría haber estado manejando sus resultados.

cdleary
fuente
3
@Tino Depende de cómo defina busy-wait. Consulte ¿Cuál es la diferencia entre espera ocupada y sondeo?
Piotr Dobrogost
1
¿Hay alguna forma de sondear un conjunto de procesos, no solo uno?
Piotr Dobrogost
1
nota: podría bloquearse si un proceso genera suficiente salida. Debe consumir stdout al mismo tiempo si usa PIPE (hay (demasiadas pero no suficientes) advertencias en los documentos del subproceso al respecto).
jfs
@PiotrDobrogost: puede usar os.waitpiddirectamente lo que permite verificar si algún proceso hijo ha cambiado su estado.
jfs
5
uso en ['/usr/bin/my_cmd', '-i', path]lugar de['/usr/bin/my_cmd', '-i %s' % path]
jfs
11

Lo que me pregunto es si este [os.system ()] es la forma correcta de lograr tal cosa.

No. os.system()no es la forma correcta. Por eso todo el mundo dice que lo use subprocess.

Para obtener más información, lea http://docs.python.org/library/os.html#os.system

El módulo de subprocesos proporciona instalaciones más potentes para generar nuevos procesos y recuperar sus resultados; Es preferible usar ese módulo que usar esta función. Utilice el módulo de subproceso. Verifique especialmente la sección Reemplazo de funciones antiguas con el módulo de subproceso.

S.Lott
fuente
8

He tenido mucho éxito con el módulo asyncproc , que se ocupa muy bien de la salida de los procesos. Por ejemplo:

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é
fuente
¿Es esto en algún lugar de github?
Nick
Es licencia gpl, así que estoy seguro de que está ahí muchas veces. Aquí hay uno: github.com/albertz/helpers/blob/master/asyncproc.py
Noah
Agregué una esencia con algunas modificaciones para que funcione con python3. (reemplaza principalmente la cadena con bytes). Ver gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic
1
Además, debe leer la salida una vez más después de salir del bucle o perderá parte de la salida.
Tic
7

Usar pexpect con readlines que no bloquean es otra forma de hacer esto. Pexpect resuelve los problemas de interbloqueo, le permite ejecutar fácilmente los procesos en segundo plano y ofrece formas sencillas de tener devoluciones de llamada cuando su proceso escupe cadenas predefinidas y, en general, hace que la interacción con el proceso sea mucho más fácil.

Gabe
fuente
4

Teniendo en cuenta "No tengo que esperar a que vuelva", una de las soluciones más fáciles será la siguiente:

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

Pero ... Por lo que leí, esta no es "la forma correcta de lograr tal cosa" debido a los riesgos de seguridad creados por la subprocess.CREATE_NEW_CONSOLEbandera.

Las cosas clave que suceden aquí es el uso de subprocess.CREATE_NEW_CONSOLEpara crear una nueva consola y .pid(devuelve el ID del proceso para que pueda verificar el programa más adelante si lo desea) para no esperar a que el programa termine su trabajo.

Pugsley
fuente
3

Tengo el mismo problema al intentar conectarme a un terminal 3270 utilizando el software de scripting s3270 en Python. Ahora estoy resolviendo el problema con una subclase de proceso que encontré aquí:

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

Y aquí está la muestra tomada del archivo:

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
fuente
3

La respuesta aceptada es muy antigua.

Encontré una mejor respuesta moderna aquí:

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

e hizo algunos cambios:

  1. hacer que funcione en windows
  2. haz que funcione con varios comandos
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 poco probable que los comandos de ejemplo funcionen perfectamente en su sistema y no maneja errores extraños, pero este código demuestra una forma de ejecutar múltiples subprocesos usando asyncio y transmitir la salida.

Terrel Shumway
fuente
Probé esto en cpython 3.7.4 ejecutándose en Windows y cpython 3.7.3 ejecutándose en Ubuntu WSL y Alpine Linux nativo
Terrel Shumway
1

Aquí hay varias respuestas, pero ninguna de ellas cumplió con mis requisitos a continuación:

  1. No quiero esperar a que termine el comando o contaminar mi terminal con salidas de subproceso.

  2. Quiero ejecutar un script bash con redireccionamientos.

  3. Quiero admitir tuberías dentro de mi script bash (por ejemplo find ... | tar ...).

La única combinación que satisface los requisitos anteriores es:

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

Esto está cubierto por Ejemplos de subprocesos de Python 3 en "Esperar a que el comando termine de forma asincrónica":

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

El proceso comenzará a ejecutarse tan pronto como se await asyncio.create_subprocess_exec(...)haya completado. Si no ha terminado para cuando llame await proc.communicate(), esperará allí para darle su estado de salida. Si ha terminado, proc.communicate()volverá inmediatamente.

La esencia aquí es similar a la respuesta de Terrel pero creo que la respuesta de Terrels parece complicar demasiado las cosas.

Consulte asyncio.create_subprocess_execpara obtener más información.

gerrit
fuente