Сообщить об ошибке.

Запуск внешних программ из кода asyncio в Python

Асинхронное выполнение subprocess.Process модулем asyncio

В этом разделе рассматривается высокоуровневые API , используемый синтаксисом async/await для создания субпроцессов (subprocesses) и управления ими во время выполнения асинхронного кода.

Вот пример того, как модуль asyncio может запустить внешнюю программу и получить ее результат:

import asyncio

async def run(cmd):
    proc = await asyncio.create_subprocess_shell(
        cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE)

    stdout, stderr = await proc.communicate()

    print(f'[{cmd!r} exited with {proc.returncode}]')
    if stdout:
        print(f'[stdout]\n{stdout.decode()}')
    if stderr:
        print(f'[stderr]\n{stderr.decode()}')

asyncio.run(run('ls /zzz'))


# ['ls /zzz' exited with 1]
# [stderr]
# ls: /zzz: No such file or directory

Можно легко выполнять и контролировать несколько субпроцессов (subprocesses) параллельно, так как все функции субпроцесса asyncio являются асинхронными, а модуль asyncio предоставляет множество инструментов для работы с такими функциями.

Модифицируем приведенный выше пример для одновременного выполнения нескольких команд:

async def main():
    await asyncio.gather(
        run('ls /zzz'),
        run('sleep 1; echo "hello"'))

asyncio.run(main())

Содержание:


Создание субпроцессов (subprocesses).

asyncio.create_subprocess_exec(program, *args, stdin=None, stdout=None, stderr=None, loop=None, limit=None, **kwds):

Функция asyncio.create_subprocess_exec() создает субпроцесс (subprocess) и возвращает экземпляр процесса. Представляет собой сопрограмму.

Аргумент limit устанавливает ограничение буфера для оберток asyncio.StreamReader для Process.stdout и Process.stderr, если asyncio.subprocess.PIPE передается в аргументы stdout и stderr.

Смотрите документацию функции loop.subprocess_exec() цикла событий, что бы узнать значение других аргументов функции asyncio.create_subprocess_exec().

Параметр цикла loop устарел и не рекомендуется к использованию, будет удален в Python 3.10.

asyncio.create_subprocess_shell(cmd, stdin=None, stdout=None, stderr=None, loop=None, limit=None, **kwds):

Функция asyncio.create_subprocess_shell() выполняет команду командной оболочки cmd и возвращает экземпляр процесса. Представляет собой сопрограмму.

Аргумент limit устанавливает ограничение буфера для оберток asyncio.StreamReader для Process.stdout и Process.stderr, если asyncio.subprocess.PIPE передается в аргументы stdout и stderr.

Смотрите документацию функции loop.subprocess_shell() цикла событий, что бы узнать значение других аргументов функции asyncio.create_subprocess_shell().

Важно. Приложение несет ответственность за то, чтобы все пробелы и специальные символы были заключены в кавычки, чтобы избежать уязвимостей, связанных с внедрением оболочки. Можно использовать функцию shlex.quote() для правильного экранирования пробелов и специальных символов оболочки в строках, которые будут использоваться для создания команд оболочки.

Параметр цикла loop устарел и не рекомендуется к использованию, будет удален в Python 3.10.

Примечание. По умолчанию, реализация [цикла событий модуля asyncio] в Windows не поддерживает субпроцессы. Субпроцессы доступны для Windows, если используется asyncio.ProactorEventLoop, представляющий цикл событий для Windows (использует порты завершения ввода-вывода IOCP).

Также модуль asyncio имеет следующие низкоуровневые API для работы с подпроцессами:- loop.subprocess_exec(),- loop.subprocess_shell(),- loop.connect_read_pipe(),- loop.connect_write_pipe().

Константы asyncio.subprocess

asyncio.subprocess.PIPE:

Константа asyncio.subprocess.PIPE может быть передана в аргументы stdin, stdout или stderr.

Если PIPE передается в аргумент stdin, то атрибут Process.stdin будет указывать на экземпляр asyncio.StreamWriter.

Если PIPE передается аргументам stdout или stderr, атрибуты Process.stdout и Process.stderr будут указывать на экземпляры asyncio.StreamReader.

asyncio.subprocess.STDOUT:

Константа asyncio.subprocess это специальное значение, которое может использоваться как аргумент stderr и указывает, что стандартная ошибка должна перенаправляться в стандартный вывод.

asyncio.subprocess.DEVNULL:

Константа asyncio.subprocess.DEVNULL представляет собой специальное значение, которое можно использовать в качестве аргумента stdin, stdout или stderr для функций создания субпроцессов. Константа указывает, что специальный файл os.devnull будет использоваться для соответствующего потока подпроцесса.

Взаимодействие с субпроцессами.

Обе функции asyncio.create_subprocess_exec() и asyncio.create_subprocess_shell() возвращают экземпляры класса asyncio.subprocess.Process.

Экземпляр Process - это обертка высокого уровня, которая позволяет взаимодействовать с субпроцессами и следить за их завершением.

asyncio.subprocess.Process:

Класс asyncio.subprocess.Process это объект, который охватывает процессы ОС, созданные функциями asyncio.create_subprocess_exec() и asyncio.create_subprocess_shell() (описаны выше).

Этот класс имеет API, аналогичное обычному классу subprocess.Popen, но есть некоторые заметные отличия:

  • в отличие от класса subprocess.Popen, экземпляры asyncio.subprocess.Process не имеют эквивалента методу Popen.poll();
  • методы Process.communicate() и Process.wait() не имеют аргумента timeout тайм-аут. Что бы воспользоваться тайм-аутом - используйте функцию asyncio.wait_for();
  • метод Process.wait() является асинхронным, тогда как метод Popen.wait() реализован как блокирующий цикл;
  • параметр universal_newlines не поддерживается.

Этот класс не является потокобезопасным.

Атрибуты и методы экземпляра asyncio.subprocess.Process:


Process.wait():

Метод Process.wait() представляет собой сопрограмму и ждет завершения дочернего процесса. Устанавливает и возвращает атрибут Process.returncode кода возврата.

Примечание. Метод Process.wait() может привести к взаимоблокировке при использовании stdout=asyncio.subprocess.PIPE или stderr=asyncio.subprocess.PIPE. Дочерний процесс генерирует так много выходных данных, что он блокирует ожидание, пока буфер канала ОС не примет больше данных. Чтобы избежать такого поведения, используйте метод Process.communicate() при использовании каналов PIPE.

Process.communicate(input=None):

Метод Process.communicate() представляет собой сопрограмму и взаимодействует с процессом:

  • отправка данных в stdin (если входной сигнал не равен None);
  • считывание данных из stdout и stderr до тех пор, пока не будет достигнут EOF;
  • Ожидание завершения процесса.

Возвращает кортеж вида (stdout_data, stderr_data).

Необязательный аргумент input - это данные (объект байтов), которые будут отправлены дочернему процессу.

Если при записи ввода в stdin возникает исключение BrokenPipeError или ConnectionResetError, то исключение игнорируется. Это условие возникает, когда процесс завершается до того, как все данные будут записаны в стандартный ввод.

Если требуется отправить данные на stdin процесса, то процесс должен быть создан с помощью stdin=asyncio.subprocess.PIPE. Аналогично, чтобы получить в кортеже результатов что-либо, кроме None, процесс должен быть создан с аргументами stdout=asyncio.subprocess.PIPE и/или stderr=asyncio.subprocess.PIPE.

Обратите внимание, что считанные данные буферизуются в памяти, поэтому не используйте метод Process.communicate(), если размер данных большой или неограниченный!

Process.send_signal(signal):

Метод Process.send_signal() посылает сигнал signal дочернему процессу.

Примечание. В Windows SIGTERM является псевдонимом для метода Process.terminate(). CTRL_C_EVENT и CTRL_BREAK_EVENT могут быть отправлены процессам, запущенным с параметром creationflags, который включает CREATE_NEW_PROCESS_GROUP.

Process.terminate():

Метод Process.terminate() останавливает дочерний процесс.

В системах POSIX этот метод посылает signal.SIGTERM для дочернего процесса.

В Windows для остановки дочернего процесса вызывается функция Win32 API TerminateProcess().

Process.kill():

Метод Process.kill() убивает дочерний процесс.

В системах POSIX этот метод отправляет signal.SIGKILL дочернему процессу.

В Windows Этот метод является псевдонимом для метода Process.terminate().

Process.stdin:

Атрибут Process.stdin это стандартный входной поток (asyncio.StreamWriter) или None, если процесс был создан с помощью stdin=None.

Process.stdout:

Атрибут Process.stdout это стандартный выходной поток (asyncio.StreamReader) или None, если процесс был создан с помощью stdout=None.

Process.stderr:

Атрибут Process.stderr это стандартный поток ошибок (asyncio.StreamReader) или None, если процесс был создан с помощью stderr=None.

Предупреждение. Используйте метод Process.communicate(), а не Process.stdin.write(), await Process.stdout.read() или await Process.stderr.read. Это позволяет избежать взаимоблокировок из-за того, что потоки приостанавливают чтение или запись и блокируют дочерний процесс.

Process.pid:

Идентификационный номер процесса (PID).

Обратите внимание, что для процессов, созданных функцией asyncio.create_subprocess_shell(), этот атрибут является номером процесса PID созданной оболочки.

Process.returncode:

Атрибут Process.returncode возвращает код возврата процесса при выходе.

Значение None указывает на то, что процесс еще не завершен.

Отрицательное значение -N указывает на то, что дочерний элемент был прерван сигналом N (только POSIX).

Пример управления субпроцессом из асинхронного кода.

Пример использования класса asyncio.subprocess.Process для управления субпроцессом и класса asyncio.StreamReader для чтения из его стандартного вывода.

Подпроцесс создается функцией asyncio.create_subprocess_exec():

import asyncio
import sys

async def get_date():
    # строка с кодом, которую будем выполнять, 
    # ее можно заменить любой командой
    code = 'import datetime; print(datetime.datetime.now())'

    # Создаем подпроцесс и перенаправляем 
    # стандартный вывод в канал `PIPE`.
    proc = await asyncio.create_subprocess_exec(
        sys.executable, '-c', code,
        stdout=asyncio.subprocess.PIPE)

    # Читаем вывод запущенной команды.
    data = await proc.stdout.readline()
    line = data.decode('ascii').rstrip()

    # Ждем когда субпроцесс завершиться.
    await proc.wait()
    # возвращаем прочитанную строку
    return line

if __name__ == '__main__':
    date = asyncio.run(get_date())
    # выводим результат работы
    print(f"Current date: {date}")


# Current date: 2021-01-19 09:57:24.047066