Класс для реализации UNIX-демонов на PHP

в 8:11, , рубрики: daemon, php, Песочница, метки: ,

Ну начнем с того, что довольно часто приходится сталкиваться с тем, что необходимо реализовывать какую-либо серверную часть для обработки каких-то данных и т.д. Естественно, что сервеную часть удобней всего было бы реализовать в виде демона. В свое время я наткнулся на подобный класс реализации демонов написанного на Python. И вот на прошлой неделе решил написать такое же творение на PHP, вроде получилось не плохо, оценивать Вам.

Итак, начнем с того, что все исходники лежат на bitbucket.org, документация там тоже написана. Собственно вот код самого класса:

<?php
/*
Author: Petr Bondarenko
E-mail: public@shamanis.com
Date: 31 May 2012
Description: Class for create UNIX-daemon
*/

class DaemonException extends Exception {}

abstract class DaemonPHP {

    protected $_baseDir;
    protected $_chrootDir = null;
    protected $_pid;
    protected $_log;
    protected $_err;
    
    /**
    * Конструктор класса. Принимает путь к pid-файлу
    * <hh user=param> string $path Абсолютный путь к PID-файлу
    */
    final public function __construct($path=null) {
        $this->_baseDir = dirname(__FILE__);
        $this->_log = $this->_baseDir . '/daemon-php.log';
        $this->_err = $this->_baseDir . '/daemon-php.err';
        if ($path === null) {
            $this->_pid = $this->_baseDir . '/daemon-php.pid';
        } else {
            $this->_pid = $path;
        }
    }
    
    /**
    * Метод устанавливает путь log-файла
    * <hh user=param> string $path Абсолютный путь к log-файлу
    * <hh user=return> DaemonPHP
    */
    final public function setLog($path) {
        $this->_log = $path;
        return $this;
    }
    
    /**
    * Метод устанавливает путь err-файла
    * <hh user=param> string $path Абсолютный путь к err-файлу
    * <hh user=return> DaemonPHP
    */
    final public function setErr($path) {
        $this->_err = $path;
        return $this;
    }
    
    /**
    * Метод позволяет установить директорию,
    * в которую будет выполнен chroot после старта демона.
    * Данный метод служит для решения проблем безопасности.
    * <hh user=param> string $path Абсолютный путь chroot-директории 
    */
    final public function setChroot($path) {
        $this->_chrootDir = $path;
        return $this;
    }
    
    /**
    * Метод выполняет демонизацию процесса, через double fork
    */
    final protected function demonize() {
        $pid = pcntl_fork();
        if ($pid == -1) {
            throw new DaemonException('Not fork process!');
        } else if ($pid) {
            exit(0);
        }
        
        posix_setsid();
        chdir('/');
        
        $pid = pcntl_fork();
        if ($pid == -1) {
            throw new DaemonException('Not double fork process!');
        } else if ($pid) {
            $fpid = fopen($this->_pid, 'wb');
            fwrite($fpid, $pid);
            fclose($fpid);
            exit(0);
        }
        
        posix_setsid();
        chdir('/');
        ini_set('error_log', $this->_baseDir . '/php_error.log');
        
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        $STDIN = fopen('/dev/null', 'r');
        
        if ($this->_chrootDir !== null) {
            chroot($this->_chrootDir);
        }
        
        $STDOUT = fopen($this->_log, 'ab');
        if (!is_writable($this->_log))
            throw new DaemonException('LOG-file is not writable!');
        $STDERR = fopen($this->_err, 'ab');
        if (!is_writable($this->_err))
            throw new DaemonException('ERR-file is not writable!');
        $this->run();
    }
    
    /**
    * Метод возвращает PID процесса
    * <hh user=return> int PID процесса
    */
    final protected function getPID() {
        if (is_readable($this->_pid)) {
            $fpid = (int) file_get_contents($this->_pid);
            return $fpid;
        } else {
            throw new DaemonException('PID-file is not readable!');
        }
    }
    
    /**
    * Метод стартует работу и вызывает метод demonize()
    */
    final public function start() {
        if (file_exists($this->_pid)) {
            echo "Process is running on PID: " . $this->getPID() . "n";
        } else {
            echo "Starting...n";
            $this->demonize();
        }
    }
    
    /**
    * Метод останавливает демон
    */
    final public function stop() {
        if (file_exists($this->_pid)) {
            echo "Stopping ... ";
            posix_kill($this->getPID(), SIGTERM);
            unlink($this->_pid);
            echo "OKn";
        } else {
            echo "Process not running!n";
        }
    }
    
    /**
    * Метод рестартует демон последовательно вызвав stop() и start()
    */
    final public function restart() {
        $this->stop();
        $this->start();
    }
    
    /**
    * Метод проверяет работу демона
    */
    final public function status() {
        if (file_exists($this->_pid)) {
            echo "Process is running on PID: " . $this->getPID() . "n";
        } else {
            echo "Process not running!n";
        }
    }
    
    /**
    * Метод обрабатывает аргументы командной строки
    */
    final public function handle($argv) {
        switch ($argv[1]) {
            case 'start':
                $this->start();
                break;
            case 'stop':
                $this->stop();
                break;
            case 'restart':
                $this->restart();
                break;
            case 'status':
                $this->status();
                break;
            default:
                echo "Unknown command!nUse: " . $argv[0] . " start|stop|restart|statusn";
                break;
        }
    }
    
    /**
    * Основной класс демона, в котором выполняется работа.
    * Его необходимо переопределить
    */
    abstract public function run();
}
?>

Сразу извинюсь за то, что код не слишком подробно откомментирован, обещаю исправить. Пока что написал только секции phpdoc. Для реализации своего демона нужно наследоваться от класса DaemonPHP и реализовать абстрактный метод run(), в котором и будет код вашего демона:

<?php
require_once 'daemon.php';

class MyDaemon extends DaemonPHP {

    public function run() {
        while (true) {
        }
    }
}

$daemon = new MyDaemon('/tmp/test.pid');

$daemon->setChroot('/home/shaman/work/PHPTest/daemon') //Устанавливаем каталог для chroot
        ->setLog('/my.log')
        ->setErr('/my.err') //После chroot файлы будут созданы в /home/shaman/work/PHPTest/daemon
        ->handle($argv);
}
?>

Рассмотрим описанный выше код. Мы создали класс MyDaemon, который наследует абстрактный класс DaemonPHP. Все методы в классе DaemonPHP объявлены, как final, кроме одного — это абстрактный метод run(). В тело этого метода помещается код, который должен выполнять Ваш демон. В нашем случае это просто пустой бесконечный цикл, чтобы увидеть работу демона. Далее мы создали объект $daemon класса MyDaemon, в конструктор передается абсолютный путь, где будет создан PID-файл демона, если не передать этот параметр, то по-умолчанию PID-файл будет создан в том же каталоге, где лежит файл демона с именем daemon-php.pid. Далее мы устанавливаем директорию для выполнения chroot методом setChroot(), это было добавлено сразу же из соображений безопасности, но делать это не обязательно. Кстати, для выполнения chroot может потребоваться запуск демона от root'а. Далее указываются абсолютные пути для LOG-файла и ERR-файла, если эти параметры не указаны, то будут созданы файлы daemon-php.log и daemon-php.err в текущей директории. В дальнейшем я думаю расширить конструктор, чтобы все эти опции можно было передавать сразу в конструктор. Далее мы вызываем метод handle(), в который передаем аргументы командной строки $argv. Этот метод сделан специально для того, чтобы Вы не думали о создании конструкции switch-case для обработки аргументов командной строки. Но, тем не менее вы можете не вызывать этот метод, а сделать что-то свое, у класса есть публичные методы start(), stop(), restart(), status(). Названия методов говорят сами за себя, собственно эти же аргументы ожидает получить handle().

Обращу ваше внимание на то, что в текущей директории может появится файл php_error.log, он появляется только тогда, когда возникают ошибки в самом PHP и пишет лог этих ошибок в него.
Сохраняем файл с кодом, например под именем run.php и запускаем:

user@localhost:~$ php run.php start
Starting...
user@localhost:~$

Статус проверить можно соответствующей командой:

user@localhost:~$ php run.php status
Process is running on PID: 6539

Ну и соответственно останавливаем демон:

user@localhost:~$ php run.php stop
Stopping ... OK

Самый свежий код этого класса всегда доступен на репозитории (ссылка была выше). Ну вот и все, жду Ваших комментариев и советов по доработке и дополнению функционала.

Автор: shamanis


  1. Саня:

    А какая вообще была цель double fork использовать? Без него ведь гораздо лучше всё работает. Пришлось ваш класс сильно переписать, чтобы он хоть как-то заработал.
    А подключить класс работы с БД была вообще целая история, потому что дочерний процесс не видел ничего из родительского и, соответственно, ни класс работы с БД, ни композер, ничего не работало…
    Второй минус в том, что класс stop вообще не проверяет, жив ли процесс или давно отвалился. Он по определению думает, что процесс жив, если его номер записан в файл. Но это же в корне не правильно. Тоже пришлось переписывать. Сейчас еще start придется переписывать, потому что он тоже тупо смотрит на запись в файле и совершенно никак не смотрит на то, жив ли вообще этот процесс.

    Может перепишите по нормальному?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js