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

Фоновое выполнение команды с модулем sh в Python

Модуль sh предоставляет несколько методов для выполнения команд и получения выходных данных неблокирующим способом.

Содержание:


Асинхронное выполнение с помощью инкрементной итерации.

Можно создавать асинхронные команды, повторяя их с помощью специального ключевого аргумента _iter. Это создает итерацию (в частности, генератор), которую можно циклически перебирать:

from sh import tail

# цикл будет работать вечно (если не поставить 
# таймаут или убить процесс сигналом)
for line in tail("-f", "/var/log/some_log_file.log", _iter=True):
    print(line)

По умолчанию ключевой аргумент _iter повторяет STDOUT, но можно изменить этот параметр, передав либо 'err', либо 'out' (вместо True). Кроме того, по умолчанию, выходные данные буферизуются строками, поэтому тело цикла будет выполняться только тогда, когда процесс создает новую строку. Можно изменить это поведение, изменив размер буфера вывода команды с помощью _out_bufsize.

Примечание. Если нужен полностью неблокирующий итератор, то используйте специальный ключевой аргумент _iter_noblock. Если текущая итерация будет заблокирована, то будет возвращен errno.EWOULDBLOCK, в противном случае получите кусок вывода.

Запуск команды в фоновом процессе.

По умолчанию каждая запущенная команда терминала через модуль sh блокируется до завершения. Если какая-то команда выполняется длительное время, то можно запустить ее в фоновом режиме с помощью специального ключевого аргумента _bg=True:

import sh
# блокирующий вызов
sh.sleep(3)
print("...3 секунды спустя")

# НЕ блокирующий вызов
p = sh.sleep(3, _bg=True)
print("печать сразу после запуска!")
# ждем завершения `sh.sleep` 
p.wait()
print("...и 3 секунды спустя")

Наверное заметили, чтобы возвратить результат после выхода из команды нужно вызвать метод RunningCommand.wait().

Команды, запущенные в фоновом режиме, игнорируют SIGHUP, это означает, что когда их контролирующий процесс (лидер сеанса, если есть управляющий терминал) завершается, то ядро ​​не сигнализирует о их завершении. Но поскольку команды модуля sh по умолчанию запускают свои процессы в своих собственных сеансах, то есть они сами являются лидерами сеансов, игнорирование SIGHUP обычно не оказывает никакого влияния. Таким образом, единственный случай, когда игнорирование SIGHUP будет делать что-либо, - это использование ключевого аргумента _new_session=False, и в этом случае контролирующим процессом, вероятно, будет оболочка, из которой запустили python, и выход из этой оболочки обычно отправляет SIGHUP всем дочерним процессам.

Использование обратных вызовов с фоновым процессом.

В сочетании с _bg=True, модуль sh может использовать обратные вызовы для постепенной обработки выходных данных, передавая вызываемую функцию в ключевые аргументы _out и/или _err. Этот вызываемый объект будет вызываться для каждой строки (или фрагмента) данных, которые выводит команда терминала:

from sh import tail

# обратный вызов
def process_output(line):
    print(line)

p = tail("-f", "/var/log/some_log_file.log", _out=process_output, _bg=True)
p.wait()

Чтобы указать, получает ли обратный вызов строку или фрагмент, используйте ключевой аргумент _out_bufsize. Чтобы "выйти" из обратного вызова, необходимо, что бы функция обратного вызова возвратила True. Это говорит команде больше не вызывать функцию обратного вызова.

Примечание. Возврат True не убивает процесс, он только предотвращает повторный вызов обратного вызова. Смотрите "Интерактивные обратные вызовы", чтобы узнать, как убить процесс из обратного вызова.

Интерактивные обратные вызовы.

Команды, вызываемые при помощи модуля sh могут взаимодействовать с базовым процессом в интерактивном режиме через определенную сигнатуру обратного вызова. Каждая команда, запущенная через sh, имеет внутреннюю очередь STDIN, которую можно использовать из обратных вызовов:

import sh

def interact(line, stdin):
    if line == "Какова... скорость ласточки?":
        stdin.put("Африканская или европейская ласточка?")
    elif line == "Хм? Я ... этого не знаю":
        cross_bridge()
        return True
    else:
        stdin.put("Я не знаю.... AAGGHHHHH")
        return True

p = sh.bridgekeeper(_out=interact, _bg=True)
p.wait()

Примечание. Если используете очередь, то можно сигнализировать об окончании ввода (EOF) с помощью значения очереди None.

Можно также убить или завершить процесс или отправить любой сигнал, из обратного вызова, добавив третий аргумент в callback функцию для получения объекта процесса:

import sh

def process_output(line, stdin, process):
    print(line)
    if "ERROR" in line:
        process.kill()
        return True

p = sh.tail("-f", "/var/log/some_log_file.log", _out=process_output, _bg=True)

Приведенный выше код будет выполняться, печатая строки из some_log_file.log до тех пор, пока в строке не появится слово 'ERROR', после чего процесс команды tail будет убит и сценарий завершится.

Примечание. Можно также использовать RunningCommand.terminate() для отправки SIGTERM или RunningCommand.signal() для отправки другого сигнала.

Обратный вызов при завершении команды.

Функция обратного вызова, переданная ключевому аргументу с именем _done вызывается, когда процесс завершается, либо обычно (через успешный или ошибочный код выхода), либо через сигнал.

Вот пример использования ключевого аргумента _done для создания многопроцессорного пула, где sh.your_parallel_command выполняется одновременно не более 10 раз:

import sh
from threading import Semaphore

pool = Semaphore(10)

def done(cmd, success, exit_code):
    pool.release()

def do_thing(arg):
    pool.acquire()
    return sh.your_parallel_command(arg, _bg=True, _done=done)

procs = []
for arg in range(100):
    procs.append(do_thing(arg))

[p.wait() for p in procs]