Простой запуск и контроль фоновых процессов в PHP и Python

Часто бывает так, что на сервере нужно выполнить длительный процесс, имея возможность отслеживать состояние его выполнения сторонними скриптами.

  • Процесс граббинга сторонних Интернет-ресурсов
  • Импорт некоего большого файла
  • Архивация файлов в файловой системе (например, если вы пишете веб-панель управления файлами)
  • Синхронизация с удалённой базой данных (например, 1С)
  • Обработка большого количества файлов (пересжатие картинок)

для всего этого можно без проблем написать консольные скрипты, работающие на любом языке программирования. Но когда нужно запускать эти скрипты из вашей программы, веб-приложения, или другого скрипта, возникают некоторые проблемы:

1) Как запустить долгоживущий процесс независимо от главного, чтобы по завершении главного процесса (того кто запускает), дочерний продолжил работу?
2) Как получать от запущенного долгоживущего процесса информацию о его прогрессе (если таковая имеет смысл), а также прочие данные о состоянии, из других процессов?
3) Как передавать эту информацию изнутри долгоживущего процесса?
4) Как быть точно уверенным, что процесс всё ещё работает, или наоборот, что он уже завершился?
5) Как при этом не терять информацию о процессах которые уже завершены?
6) Как добиться того, чтобы долгоиграющие процессы запускались от имени не того пользователя, кто их запускает, а от другого?

У данных проблем есть решения как средствами Unix (файлы pid, команды ps, screen, nohup, sudo в связке с bash, sed, awk), так и специализированными мощными инструментами, позволяющими запускать тысячи процессов, распараллеливать по ним задачи, балансировать с их помощью нагрузку на сервера и так далее.

Я же предлагаю простой, удобный инструмент, предназначенный для решения вопросов запуска и контроля фоновых процессов и ничего больше. Он даёт ответы на все поставленный вопросы, практически не имеет конфигурации, и элементарно прост в использовании.

Изначально данный инструмент я писал на Gambas’е (аналог Visual Basic для Linux), но позже переписал на Python 3.

Демонизатор процессов

Инструмент назван pdem от process demonizer.

Назначение:

  • Работа в качестве сервера, управление при помощи простого протокола по TCP/IP
  • Запуск приложений по команде от управляющего клиента
  • Остановка приложения по команде от управляющего клиента
  • Мониторинг работающего приложения:
    • Отслеживание состояния (работает/умерло), учёт времени работы
    • Получение от приложения информации о его состоянии и хранение этой информации (прогресс выполнения, произвольные переменные)
  • Получение информации о работающем приложении из клиента
  • Долговременная работа сервера, может работать месяцы и годы без утечек памяти

h2>Установка

Установка в Debian Jessie:

# aptitude install python3 python3-pip
# pip3 install pdem

Принцип работы и использование из консоли

pdem состоит из 3-х основных компонентов:
1) сервер – выполняет процессы, контролирует их состояние, общается с клиентами
2) клиент – позволяет подключаться к серверу, и давать ему команды, такие как запуск процессов, остановка, получение сведений о состоянии процессов. Клиент можно запускать из консоли, также есть клиентские библиотеки на Pyhton3 и на PHP.
3) библиотека для передачи сообщений из процессов серверу – в принципе, из bash-скрипта передать серверу сообщения можно с помощью команды echo, так как сервер просто перехватывает стандартный вывод процесса, но клиентская PHP-библиотека также содержит в себе код для передачи в стандартный вывод параметров серверу pdem.

Принцип работы:

1) После запуска сервер ждёт подключений от клиентов.
Проще всего запустить сервер так:

$ pdem start

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

$ pdem start --daemonize yes

2) Клиент после подключения может передать серверу команду, которую сервер выполняет, и отвечает клиенту неким результатом.

$ pdem do [commandname]

Примеры команд (подробнее в help-е):
help – заставляет сервер возвратить клиенту полный список и описание поддерживаемых

pdem do help

runprocess – приказывает серверу запустить процесс

pdem do runprocess process_name process_title local "bash -c /home/user/log_script.sh"

где:
process_name – уникальное имя процесса, по которому он идентифицируется
process_title – заголовок процесса, некое человеко-читаемое название
local – тип процесса, пока ничего кроме local не поддерживается
остальная часть команды – это собственно команда, которую нужно запустить.
proclist – получить список процессов
получаем список процессов:

pdem do proclist

получаем список процессов вместе с уже дохлыми:

pdem do proclist showdead

3) После запуска процесса сервер будет считывать данные из его стандартного вывода, ожидая там специально сформированных строк с информацией о состоянии процесса.

Например, если процесс выведет сообщение [PDEM[progressenabled]PDEM], то сервер включит для него режим отслеживания проресса.

При выдаче процессом сообщения [PDEM[progess=30]PDEM], сервер запомнит, что прогресс выполнения процесса 30%, и будет вычислять примерное оставшееся время исходя из этого.

Также, с помощью инструкции вроде [PDEM[var:myvar=123]PDEM] процесс может сообщить серверу произвольную информацию о своём состоянии (123 в данном случае), которую сервер сохранит в переменную с именем myvar (в данном случае). Таких переменных может быть сколько угодно.

Таким образом, процесс может сообщать серверу любую информацию о своей работе, а клиент сможет, запросив у сервера список процессов, получить также и все данные состояния клиента – его прогресс, время выполнения, примерное оставшееся время и все дополнительные переменные, созданные процессом.

Простота данного протокола позволяет легко доработать скрипт на bash чтобы он выдавал данные информационные сообщения. Также, имеется библиотека на PHP и Python для вывода таких сообщений, что пригодится если ваш долгоиграющий скрипт написан на PHP или Python.

Подробная документация

Основные команды pdem

pdem help справка

pdem start запустить сервер

pdem status подключиться к серверу и сообщить его статус

pdem stop подключиться к серверу и дать ему команду на отключение

pdem do [имя команды] подключиться к серверу передать ему команду, после чего вывести его ответ.

pdem writeconf записать конфигурацию по умолчанию в файл /tmp/pdem-server.conf.example. Если указаны дополнительные опции (из перечисленных ранее), то в конфигурационный файл будут переданы они.

Опции и настройки

Как клиент так и сервер используют ряд настроек в своей работе. Эти настройки могут быть переданы при запуске из командной строки, указаны в конфигурационном файле, либо будут подразумеваться их значения по умолчанию.
При передаче в командной строке, настройки имеют формат “–имя_настройки значение”.

Имеющиеся настройки:

–daemonize, значение может быть один из вариантов: 0,1,yes,no,true,false. Эта настройка используется только при запуске сервера, и определяет, нужно ли серверу стать демоном и работать в фоновом режиме, или нет. По умолчанию false.
–listenAddr, значение должно быть IP-адресом одного из имеющихся в системе сетевых интерфейсов, и определяет, на каком интерфейсе сервер будет ожидать соедниение, и на каком порту клиент будет рассчитывать найти сервер при подключении к нему. По умолчанию 127.0.0.1
–listenPort, значение должно быть числом от 0 до 65535, это номер порта на котором слушает сервер, и на который подключается клиент. По умолчанию 5555.
–daemonLogFile, значение должно быть путём к лог-файлу, в который пишется выдача информации из сервера-демона. Если не указан, будет писаться в /tmp/pdem-server.log (так как папка /var/log по умолчанию доступна для записи далеко не всем пользователям).
–logLevel, значение одно из DEBUG, INFO, NOTICE, WARNING, ERROR. Определяет с какого уровня нужно начинать выводить сообщения. При указании уровня, все следующие уровни также включаются. Например, если выбран уровень NOTICE, то будут отображены все сообщения имеющие уровень NOTICE, WARNING и ERROR. Сообщения будут выдаваться в стандартный вывод, если сервер запущен в интерактивном режиме, или в daemonLogFile, если запущен в режиме демона. По умолчанию значение NOTICE
–conf, значение должно быть путём к файлу настроек. По умолчанию путь к файлу “~/.config/pdem.conf”.

Настройки в конфигурационном файле:

Все те же настройки (кроме опции –conf) могут быть указаны в конфигурационном файле:

listenPort = 5555
daemonLogFile = /tmp/pdem-server.log
listenAddr = 127.0.0.1
daemonize = 0
logLevel = DEBUG

Библиотека клиента на Python3

Для обмена данными с приложением на Python3 в pdem встроена клиентская библиотека. Сразу после установки пакета pdem она будет доступна.

Использование:

# Подключаем модуль
from pdem import pdem
import os
# Подключаемся к серверу
client = pdem.PdemClient("127.0.0.1", 5555)
 
# Запускать будем тестовый скрипт (файл тестового скрипта есть в репозитории)
cmd = os.path.realpath("./bash/test_script.sh")
# Запускаем процесс скрипта
client.runprocess("process1", "Заголовок процесса", cmd)
# Получаем список запущеных процессов и печатаем его
client.proclist(
    lambda results: print(results)
)

Все команды асинхронные, однако их можно выполнять одну за другой, не боясь, что вторая будет выполнена раньше чем первая. Сервер pdem обслуживает команды каждого клиента последовательно, и, в рамках одного подключения, не может выполнить вашу вторую команду быстрее чем первую.

Библиотека клиента на PHP

Установка библиотеки

Установить библиотеку можно одним из способов:

1) Склонировать репозитрий:

git clone https://github.com/MihanEntalpo/pdem-php

2) Установить пакет из composer:

Содержимое файла composer.json:

{
    "require":{
        "mihanentalpo/pdem-php": "*"
    }
}

Установка:

composer install

Применение:

<?php
//Если устанавливали с помощью композера:
require_once("./vendor/autoload.php");
//Если устанавливали путём простого клонирования или скопировав файл:
require_once("./classPdem.php");
//Подключаемся к серверу
pdem::connect("127.0.0.1", 5555);
//Выполняем процесс на сервере
$path = realpath("./test_script.sh");
pdem::runLocal("test_script1", "Тестовый скрипт 1", $path);
//Получим список процессов и выведем его:
print_r(pdem::proclist(true, "", true));
//Убъём процесс
pdem::kill("test_script1");

Применение при написании долгоиграющих скриптов на PHP

Если ваш скрипт, запускаемый посредством pdem, написан на PHP, для передачи сообщений в pdem можно использовать следующие функции:

<?php
//Если устанавливали с помощью композера:
require_once("./vendor/autoload.php");
//Если устанавливали путём простого клонирования или скопировав файл:
require_once("./classPdem.php");
 
//Сообщить о том, что ваш процесс поддерживает отслеживание процентов выполнения
pdem::console_progress_enabled();
//Сообщить текущий процент выполнения:
pdem::console_progress(15);
//Передать в pdem произвольные переменные, 
//которые что-то будут значить для интерфейса, 
//использующего pdem для контроля процессов:
pdem::console_vars(array(
    "state" => "Первый этап",
    "mode" => 1
));

So, what do you think ?