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

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

<?php
/*
Author: Petr Bondarenko
E-mail: public@shamanis.com
Date: 31 May 2012
License: BSD
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-файлу
    * @param string $path Абсолютный ��уть к PID-файлу
    */
    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-файла
    * @param string $path Абсолютный путь к log-файлу
    * @return DaemonPHP
    */
    final public function setLog($path) {
        $this->_log = $path;
        return $this;
    }
    
    /**
    * Метод устанавливает путь err-файла
    * @param string $path Абсолютный путь к err-файлу
    * @return DaemonPHP
    */
    final public function setErr($path) {
        $this->_err = $path;
        return $this;
    }
    
    /**
    * Метод позволяет установить директорию,
    * в которую будет выполнен chroot после старта демона.
    * Данный метод служит для решения проблем безопасности.
    * @param string $path Абсолютный путь chroot-директории 
    */
    final public function setChroot($path) {
        if (!function_exists('chroot')) {
            throw new DaemonException('Function chroot() has no. Please update you PHP version.');
        }
        $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 процесса
    * @return int PID процесса
    */
    final protected function getPID() {
        if (file_exists($this->_pid)) {
            $pid = (int) file_get_contents($this->_pid);
            if (posix_kill($pid, SIG_DFL)) {
                return $pid;
            } else {
                //Если демон не откликается, а PID-файл существует
                unlink($this->_pid);
                return 0;
            }
        } else {
            return 0;
        }
    }
    
    /**
    * Метод стартует работу и вызывает метод demonize()
    */
    final public function start() {
        if (($pid = $this->getPID()) > 0) {
            echo "Process is running on PID: " . $pid . PHP_EOL;
        } else {
            echo "Starting..." . PHP_EOL;
            $this->demonize();
        }
    }
    
    /**
    * Метод останавливает демон
    */
    final public function stop() {
        if (($pid = $this->getPID()) > 0) {
            echo "Stopping ... ";
            posix_kill($pid, SIGTERM);
            unlink($this->_pid);
            echo "OK" . PHP_EOL;
        } else {
            echo "Process not running!" . PHP_EOL;
        }
    }
    
    /**
    * Метод рестартует демон последовательно вызвав stop() и start()
    */
    final public function restart() {
        $this->stop();
        $this->start();
    }
    
    /**
    * Метод проверяет работу демона
    */
    final public function status() {
        if (($pid = $this->getPID()) > 0) {
            echo "Process is running on PID: " . $pid . PHP_EOL;
        } else {
            echo "Process not running!" . PHP_EOL;
        }
    }
    
    /**
    * Метод обрабатывает аргументы командной строки
    */
    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!" . PHP_EOL .
                    "Use: " . $argv[0] . " start|stop|restart|status" . PHP_EOL;
                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


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