Como posso executar um comando externo de forma assíncrona do Python?

120

Preciso executar um comando shell de forma assíncrona de um script Python. Com isso, quero dizer que quero que meu script Python continue em execução enquanto o comando externo dispara e faz tudo o que precisa ser feito.

Eu li esta postagem:

Chamar um comando externo em Python

Então saí e fiz alguns testes, e parece que os.system()vou fazer o trabalho desde que eu use &no final do comando para não ter que esperar o retorno. O que me pergunto é se essa é a maneira correta de realizar tal coisa. Tentei commands.call()mas não funcionou para mim porque bloqueia no comando externo.

Informe-me se os.system()é aconselhável usar para isso ou se devo tentar alguma outra rota.

Comunidade
fonte

Respostas:

135

subprocess.Popen faz exatamente o que você deseja.

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

(Edite para completar a resposta dos comentários)

A instância do Popen pode fazer várias outras coisas, como você pode poll()fazer para ver se ainda está em execução, e você pode communicate()enviar dados em stdin e esperar que ele seja encerrado.

Ali Afshar
fonte
4
Você também pode usar poll () para verificar se o processo filho foi encerrado ou usar wait () para esperar que ele termine.
Adam Rosenfield
Adam, é verdade, embora pudesse ser melhor usar o comunicado () para esperar, porque isso tem um melhor manuseio de buffers de entrada / saída e há situações em que o flood pode bloquear.
Ali Afshar
Adam: os documentos dizem "Aviso Isso causará um impasse se o processo filho gerar saída suficiente para um canal stdout ou stderr de forma que bloqueie a espera que o buffer do canal do sistema operacional aceite mais dados. Use comunic () para evitar isso."
Ali Afshar
14
Communic () e wait () são operações de bloqueio. Você não paralelizará comandos como o OP parece perguntar se você os usa.
cdleary
1
Cdleary está absolutamente correto, deve ser mencionado que comunique e espere para bloquear, então só faça isso quando estiver esperando que as coisas sejam encerradas. (O que você realmente deve fazer para se comportar)
Ali Afshar
48

Se você deseja executar muitos processos em paralelo e, em seguida, manipulá-los quando eles gerarem resultados, você pode usar a pesquisa como a seguir:

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)

O fluxo de controle é um pouco complicado porque estou tentando torná-lo pequeno - você pode refatorar a seu gosto. :-)

Isso tem a vantagem de atender primeiro às solicitações de finalização antecipada. Se você chamar communicateo primeiro processo em execução e ele for executado por mais tempo, os outros processos em execução ficarão parados, ociosos, quando você poderia estar lidando com seus resultados.

cdleary
fonte
3
@Tino Depende de como você define a espera ocupada. Consulte Qual é a diferença entre busy-wait e polling?
Piotr Dobrogost de
1
Existe alguma maneira de pesquisar um conjunto de processos e não apenas um?
Piotr Dobrogost de
1
nota: ele pode travar se um processo gerar saída suficiente. Você deve consumir stdout simultaneamente se usar PIPE (há (muitos, mas não o suficiente) avisos nos documentos do subprocesso sobre isso).
jfs de
@PiotrDobrogost: você pode usar os.waitpiddiretamente o que permite verificar se algum processo filho mudou de status.
jfs
5
use em ['/usr/bin/my_cmd', '-i', path]vez de['/usr/bin/my_cmd', '-i %s' % path]
jfs
11

O que estou querendo saber é se este [os.system ()] é a maneira correta de fazer isso?

Não. Não os.system()é a maneira correta. É por isso que todo mundo diz para usar subprocess.

Para obter mais informações, leia http://docs.python.org/library/os.html#os.system

O módulo de subprocesso fornece recursos mais poderosos para gerar novos processos e recuperar seus resultados; usar esse módulo é preferível a usar esta função. Use o módulo de subprocesso. Verifique especialmente a seção Substituindo funções mais antigas pelo módulo de subprocesso.

S.Lott
fonte
8

Tive bom sucesso com o módulo asyncproc , que lida muito bem com a saída dos processos. Por exemplo:

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é
fonte
isso está em algum lugar do github?
Nick
É uma licença gpl, então tenho certeza que está lá muitas vezes. Aqui está um: github.com/albertz/helpers/blob/master/asyncproc.py
Noah
Eu adicionei uma essência com algumas modificações para fazê-la funcionar com o python3. (principalmente substitui o str por bytes). Consulte gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic
1
Além disso, você precisa ler a saída mais uma vez depois de sair do loop ou perderá parte da saída.
Tic
7

Usar o pexpect com linhas de leitura sem bloqueio é outra maneira de fazer isso. O Pexpect resolve os problemas de deadlock, permite que você execute facilmente os processos em segundo plano e oferece maneiras fáceis de ter callbacks quando o seu processo gera strings predefinidas e geralmente torna a interação com o processo muito mais fácil.

Gabe
fonte
4

Considerando "Não tenho que esperar que volte", uma das soluções mais fáceis será esta:

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

Mas ... Pelo que li, esta não é "a maneira correta de realizar tal coisa" por causa dos riscos de segurança criados por subprocess.CREATE_NEW_CONSOLEflag.

As principais coisas que acontecem aqui é o uso de subprocess.CREATE_NEW_CONSOLEpara criar um novo console e .pid(retorna o ID do processo para que você possa verificar o programa mais tarde se desejar) para não esperar que o programa termine seu trabalho.

Pugsley
fonte
3

Tenho o mesmo problema ao tentar me conectar a um terminal 3270 usando o software de script s3270 em Python. Agora estou resolvendo o problema com uma subclasse de Processo que encontrei aqui:

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

E aqui está a amostra retirada do arquivo:

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

A resposta aceita é muito antiga.

Encontrei uma resposta moderna melhor aqui:

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

e fez algumas alterações:

  1. faça funcionar no windows
  2. faça funcionar com vários 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)))

É improvável que os comandos de exemplo funcionem perfeitamente em seu sistema e não lida com erros estranhos, mas este código demonstra uma maneira de executar vários subprocessos usando asyncio e transmitir a saída.

Terrel Shumway
fonte
Eu testei isso no cpython 3.7.4 rodando no windows e no cpython 3.7.3 rodando no Ubuntu WSL e Alpine Linux nativo
Terrel Shumway
1

Existem várias respostas aqui, mas nenhuma delas atendeu aos requisitos abaixo:

  1. Não quero esperar o comando terminar ou poluir meu terminal com saídas de subprocesso.

  2. Quero executar o script bash com redirecionamentos.

  3. Quero oferecer suporte a tubulação em meu script bash (por exemplo find ... | tar ...).

A única combinação que satisfaz os requisitos acima é:

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

Isso é abordado nos exemplos de subprocesso do Python 3 em "Aguarde o comando terminar de forma assíncrona":

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

O processo começará a ser executado assim que o await asyncio.create_subprocess_exec(...)for concluído. Se não tiver terminado no momento em que você ligar await proc.communicate(), ele aguardará para fornecer o status de saída. Se tiver terminado, proc.communicate()vai voltar imediatamente.

A essência aqui é semelhante à resposta de Terrel, mas acho que a resposta de Terrel parece complicar as coisas.

Veja asyncio.create_subprocess_execpara mais informações.

gerrit
fonte