gnifajio
@gnifajio
Совершенствуюсь каждый день

Как запустить shell-комманду в новом процессе без блокировки потока?

Есть асинхронное приложение, в котором нужно вызвать произвольную shell-комманду и получить ее вывод, при этом не заблокировать остальные потоки.
Пока что я додумался только до этого:
from subprocess import Popen, TimeoutExpired, PIPE

async def call_process(command: str, timeout: int = 5) -> None:
    try:
        proc = Popen(command, stdout=PIPE, stderr=PIPE, shell=True)
        stdout, stderr = proc.communicate(timeout=timeout)
        exitcode = proc.returncode
    except TimeoutExpired:
        print(f'Command {command} timed out ({timeout}s)')
    else:
        print(
            f'Exitcode: {exitcode}\n' + (
                "STDERR: " + stderr.decode()
                if stderr else "STDOUT: " + stdout.decode() if stdout else ''
            )
        )


Проблема этого кода в том, что пока оно работает, все приложение не отвечает до того момента, пока команда не выполнится, или не истечет время ожидания.

Я пробовал также multithreading, но так как функция асинхронная, то отрабатывать не хочет.
Также пробовал subprocess.check_output, результат такой же как и от Popen
Предвидя ответы "сделай функцию синхронной" отвечаю: нельзя, вместо print там вызов async функции.

P.S.: И да, я гуглил и читал доки
P.P.S: loop.run_in_executor() не сработал
  • Вопрос задан
  • 71 просмотр
Решения вопроса 1
gnifajio
@gnifajio Автор вопроса
Совершенствуюсь каждый день
В данном случае помогло asyncio run_in_executor
Спасибо Сергей Горностаев и Vindicar, которые посоветовали мне это.
Я преобразовал код вот так:
async def call_process(command: str, timeout: int = 5):
    def caller(cmd: str, t_out):
        try:
            proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
            stdout, stderr = proc.communicate(timeout=t_out)
            exitcode = proc.returncode
        except TimeoutExpired:
            return f'Command "{cmd}" timed out ({t_out}s)'
        else:
            return f'Exitcode: {exitcode}' + (
                f'STDERR: {stderr.decode()}'
                if stderr else f'STDOUT: {stdout.decode()}' if stdout else ''
                )

    loop = asyncio.get_running_loop()
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(
            pool, caller, command, timeout)
    await some_async_func(result)

Конкретно для моего случая это подходит идеально.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
Vindicar
@Vindicar
RTFM!
communicate() как раз и ждёт. Вместо этого ты можешь попробовать просто читать из proc.stdout.
С асинхронностью сложнее, но ты можешь попробовать запустить функцию в потоке через loop.run_in_executor(). Это позволит завернуть поток в асинхронную задачу. Так как поток будет висеть в ожидании ввода-вывода, он не должен сильно влиять на GIL и мешать другим потокам.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы