Bagaimana saya bisa menjalankan perintah eksternal secara asynchronous dari Python?

120

Saya perlu menjalankan perintah shell secara asinkron dari skrip Python. Maksud saya, saya ingin skrip Python saya terus berjalan saat perintah eksternal mati dan melakukan apa pun yang perlu dilakukan.

Saya membaca posting ini:

Memanggil perintah eksternal dengan Python

Saya kemudian pergi dan melakukan beberapa pengujian, dan sepertinya os.system()akan melakukan pekerjaan asalkan saya gunakan &di akhir perintah sehingga saya tidak perlu menunggu untuk kembali. Yang saya pikirkan adalah apakah ini cara yang tepat untuk mencapai hal seperti itu? Saya mencoba commands.call()tetapi tidak akan berhasil untuk saya karena blok pada perintah eksternal.

Tolong beri tahu saya jika menggunakan os.system()untuk ini disarankan atau jika saya harus mencoba beberapa rute lain.

Masyarakat
sumber

Jawaban:

135

subprocess.Popen melakukan apa yang Anda inginkan.

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

(Edit untuk melengkapi jawaban dari komentar)

Instance Popen dapat melakukan berbagai hal lain seperti Anda dapat poll()melakukannya untuk melihat apakah masih berjalan, dan Anda dapat communicate()dengannya untuk mengirim data ke stdin, dan menunggu sampai berhenti.

Ali Afshar
sumber
4
Anda juga bisa menggunakan poll () untuk memeriksa apakah proses anak telah dihentikan, atau gunakan wait () untuk menunggunya berakhir.
Adam Rosenfield
Adam, sangat benar, meskipun bisa lebih baik menggunakan komunikasikan () untuk menunggu karena itu memiliki penanganan yang lebih baik dari buffer masuk / keluar dan ada situasi di mana flooding mungkin memblokir ini.
Ali Afshar
Adam: docs mengatakan "Peringatan Ini akan menemui jalan buntu jika proses anak menghasilkan keluaran yang cukup ke stdout atau stderr pipa sehingga memblokir menunggu buffer pipa OS untuk menerima lebih banyak data. Gunakan komunikasikan () untuk menghindari itu."
Ali Afshar
14
Communicator () dan wait () memblokir operasi. Anda tidak akan memparalelkan perintah seperti OP yang menanyakan apakah Anda menggunakannya.
cdleary
1
Cdleary benar sekali, harus disebutkan bahwa berkomunikasi dan menunggu do memblokir, jadi lakukan hanya ketika Anda menunggu hal-hal ditutup. (Yang benar-benar harus Anda lakukan untuk berperilaku baik)
Ali Afshar
48

Jika Anda ingin menjalankan banyak proses secara paralel dan kemudian menanganinya saat mereka membuahkan hasil, Anda dapat menggunakan polling seperti berikut:

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)

Aliran kontrol di sana sedikit berbelit-belit karena saya mencoba membuatnya kecil - Anda dapat menyesuaikan dengan selera Anda. :-)

Ini memiliki keuntungan dalam melayani permintaan penyelesaian awal terlebih dahulu. Jika Anda memanggil communicateproses yang berjalan pertama dan ternyata berjalan paling lama, proses yang berjalan lainnya akan diam di sana saat Anda seharusnya menangani hasilnya.

cdleary
sumber
3
@Tino Itu tergantung pada bagaimana Anda mendefinisikan sibuk-menunggu. Lihat Apa perbedaan antara busy-wait dan polling?
Piotr Dobrogost
1
Apakah ada cara untuk melakukan polling terhadap serangkaian proses, tidak hanya satu?
Piotr Dobrogost
1
Catatan: mungkin hang jika suatu proses menghasilkan output yang cukup. Anda harus menggunakan stdout secara bersamaan jika Anda menggunakan PIPE (ada (terlalu banyak tetapi tidak cukup) peringatan di dokumen subproses tentangnya).
jfs
@PiotrDobrogost: Anda dapat menggunakan os.waitpidsecara langsung yang memungkinkan untuk memeriksa apakah ada proses anak yang telah mengubah statusnya.
jfs
5
gunakan ['/usr/bin/my_cmd', '-i', path]sebagai pengganti['/usr/bin/my_cmd', '-i %s' % path]
jfs
11

Apa yang saya ingin tahu adalah apakah [os.system ()] ini adalah cara yang tepat untuk mencapai hal seperti itu?

Tidak, os.system()bukanlah cara yang tepat. Itulah mengapa semua orang mengatakan untuk menggunakan subprocess.

Untuk informasi lebih lanjut, baca http://docs.python.org/library/os.html#os.system

Modul subproses menyediakan fasilitas yang lebih kuat untuk menghasilkan proses baru dan mengambil hasilnya; menggunakan modul itu lebih disukai daripada menggunakan fungsi ini. Gunakan modul subproses. Periksa terutama bagian Mengganti Fungsi Lama dengan Modul subproses.

S. Lott
sumber
8

Saya telah berhasil dengan baik dengan modul asyncproc , yang berhubungan dengan baik dengan keluaran dari proses. Sebagai contoh:

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
Nuh
sumber
apakah ini ada di mana saja di github?
Nick
Ini lisensi gpl, jadi saya yakin ada di sana berkali-kali. Ini salah satunya: github.com/albertz/helpers/blob/master/asyncproc.py
Noah
Saya menambahkan inti dengan beberapa modifikasi untuk membuatnya bekerja dengan python3. (kebanyakan menggantikan str dengan byte). Lihat gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic
1
Selain itu, Anda perlu membaca output sekali lagi setelah keluar dari loop atau Anda akan kehilangan beberapa output.
Tic
7

Menggunakan pexpect dengan garis baca non-pemblokiran adalah cara lain untuk melakukan ini. Pexpect memecahkan masalah kebuntuan, memungkinkan Anda menjalankan proses dengan mudah di latar belakang, dan memberikan cara mudah untuk memiliki callback saat proses Anda mengeluarkan string yang telah ditentukan, dan umumnya membuat interaksi dengan proses menjadi lebih mudah.

Gabe
sumber
4

Mempertimbangkan "Saya tidak perlu menunggu sampai kembali", salah satu solusi termudah adalah ini:

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

Tapi ... Dari apa yang saya baca ini bukanlah "cara yang tepat untuk mencapai hal seperti itu" karena risiko keamanan yang ditimbulkan subprocess.CREATE_NEW_CONSOLE bendera.

Hal-hal utama yang terjadi di sini adalah penggunaan subprocess.CREATE_NEW_CONSOLEuntuk membuat konsol baru dan .pid(mengembalikan ID proses sehingga Anda dapat memeriksa program nanti jika Anda mau) sehingga tidak menunggu program menyelesaikan tugasnya.

Pugsley
sumber
3

Saya memiliki masalah yang sama saat mencoba menyambung ke terminal 3270 menggunakan perangkat lunak scripting s3270 dengan Python. Sekarang saya sedang memecahkan masalah dengan subkelas Proses yang saya temukan di sini:

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

Dan berikut ini contoh yang diambil dari file:

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

Jawaban yang diterima sudah sangat tua.

Saya menemukan jawaban modern yang lebih baik di sini:

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

dan membuat beberapa perubahan:

  1. membuatnya berfungsi di windows
  2. membuatnya bekerja dengan banyak perintah
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)))

Tidak mungkin bahwa contoh perintah akan bekerja dengan sempurna di sistem Anda, dan tidak menangani kesalahan aneh, tetapi kode ini menunjukkan satu cara untuk menjalankan beberapa subproses menggunakan asyncio dan mengalirkan output.

Terrel Shumway
sumber
Saya menguji ini pada cpython 3.7.4 yang berjalan di windows dan cpython 3.7.3 yang berjalan di Ubuntu WSL dan Alpine Linux asli
Terrel Shumway
1

Ada beberapa jawaban di sini tetapi tidak ada yang memenuhi persyaratan saya di bawah ini:

  1. Saya tidak ingin menunggu perintah selesai atau mencemari terminal saya dengan keluaran subproses.

  2. Saya ingin menjalankan skrip bash dengan pengalihan.

  3. Saya ingin mendukung perpipaan dalam skrip bash saya (misalnya find ... | tar ...).

Satu-satunya kombinasi yang memenuhi persyaratan di atas adalah:

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

Ini dicakup oleh Contoh Subproses Python 3 di bawah "Tunggu perintah untuk berhenti secara asinkron":

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

Prosesnya akan mulai berjalan segera setelah await asyncio.create_subprocess_exec(...)selesai. Jika belum selesai pada saat Anda menelepon await proc.communicate(), itu akan menunggu di sana untuk memberi Anda status keluaran. Jika sudah selesai, proc.communicate()akan langsung dikembalikan.

Intinya di sini mirip dengan jawaban Terrels tetapi saya pikir jawaban Terrels tampaknya terlalu rumit.

Lihat asyncio.create_subprocess_execuntuk informasi lebih lanjut.

gerrit
sumber