Пользователь
0,0
рейтинг
4 июня 2012 в 12:11

Разработка → Класс для реализации UNIX-демонов на PHP из песочницы

PHP*
Ну начнем с того, что довольно часто приходится сталкиваться с тем, что необходимо реализовывать какую-либо серверную часть для обработки каких-то данных и т.д. Естественно, что сервеную часть удобней всего было бы реализовать в виде демона. В свое время я наткнулся на подобный класс реализации демонов написанного на 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


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

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (102)

  • +6
    posix_kill($this->getPID(), 9);

    Может не стоит сразу SIGKILL посылать?
    • –7
      Честно, не вдавался в подробности сигналов. Почитаю на досуге подробней, исправлю и закомичу в репу. Там же вроде SIGTERM нужно посылать?
      • –2
        От целей зависит. Мне вот субъективно кажется, что в рассматриваемом случае KILL вполне уместен.
        • +3
          В смысле демон на PHP по определению настолько убог, что ему никогда не потребуется выполнить какую-либо зачистку перед остановкой?
          • +1
            Да, можно и так читать.
        • +6
          9-й сигнал, к сожалению, и правда лучше как можно реже использовать — потому что не все ресурсы IPC, занятые процессом, освобождаются при такой смерти. Например, семафоры и страницы shared memory нужно явно освобождать (что демоны часто делают в своих обработчиках смертельных сигналов). Я сам в такое не верил, пока не убедился на собственном опыте (в моем случае в какой-то момент ни одного процесса apache в живых не осталось, т.к. я все убил по 9-му сигналу, но ресурсы занимались и переполнились — man ipcs).
          • 0
            Вот с этим точно не поспоришь — на неосвобожденные IPC'шные очереди и семафоры, например, я и сам насмотрелся, была возможность.
    • –3
      Да раз уж метод stop() называется, вполне логично kill -9 послать. Вот если бы был метод kill(), то аргумент с номером сигнала был бы очень уместен.
      • +4
        Речь не о том, чтобы кастомизировать посылаемый сигнал. Разницу между SIGKILL и SIGTERM знаете?
        • 0
          Исправил на SIGTERM. Проверил. Закоммитил.
        • +1
          Знаю конечно. Вы когда процесс убиваете обычно, просто kill ему шлете? У меня вот стопроцентная привычка: если уж дошло до убийства, то KILL и послать. Понятно, что у вас могут быть умные красивые демоны, которые в ответ на SIGTERM запишут логи, дампы, отправят отчет по e-mail и xmpp и мирно завершатся. Ну тогда и измените метод stop().
          • 0
            Вот над этим нужно подумать еще. Как бы это первая еще «сырая» версия. Я по-этому и опубликовал, чтобы узнать мнение чего здесь не хватает.
          • +3
            Есть набор соглашений, которых рекомендуется придерживаться при разработке демонов. SIGTERM для завершения, SIGHUP для обновления конфигурации и тп.

            Правильный демон можно использоваться совместно с системой типа launchd или xinetd — и получать такие плюшки как on demand запуск и автоматический рестарт при падении.
            • +2
              Ну так-то оно так, но это скорее уже к системным сервисам относится, чем к демону как таковому.
              Здесь, как я понимаю, автор привел схему простейшей демонизации php-скрипта, главнейшая цель которой — отвязать скрипт от терминала и дать ему работать в фоне «вечно». Со своими обязанностями класс справляется.
              Предложенные вами вещи конечно неплохо бы дописать(коль уж вываливаем на всеобщее обозрение в репозитарий), но пусть они тогда будут отключаемыми.
              Ну может конфиг при создании класса передавать какой с настройками.
              У меня еще при стартах демонов выполняются такие вещи:
              ini_set("max_execution_time", "0");
              ini_set("memory_limit", "-1");
              ob_implicit_flush();
              define("IS_WIN", (stristr(php_uname('s'), 'windows')==FALSE)?false:true);
              

              Если IS_WIN, то демонизация не происходит — запускается просто как консольный скрипт. Винда — это отладочный полигон.

              =====================

              Ну и еще допишу, это уже не на ваш комментарий, а автору поста на заметку(лениво новый камент писать).

              У меня при создании подробнейших логов(tcp-сервер все-таки, писать в логи всегда есть чего) как-то обнаружилось, что логи больно дофига места занимают. И соответственно потом при жалобах, что кто-то чего-то не смог разобраться в системе, когда надо лезть в логи и смотреть, чего там юзер куда клацал и куда ломился, оказывается, что скакать по большим логам в общем-то удовольствие сомнительное.
              Поэтому были дописаны некоторые авто-функции:
              Тыц сюда
                function gen_log_subdir() {
                   $tmv=time();
                   return sprintf('%08X_', $tmv).date("dmY_His", $tmv);
                }
              
                function check_log_subdir($tsize=100000000) {
                   $root=$this->log_dir.$this->log_subdir;
                   $res=0;
                   if ($dir = @opendir($root)) {
                      while (($file = @readdir($dir)) !== false) {
                         $fn=$root.$file;
                         if (is_file($fn)) {
                             $res+=@filesize($fn);
                             if ($res>$tsize) break;
                         }
                      }
                      @closedir($dir);
                   }
                   $this->log_add=sprintf('%10u', $res);
                   return ($res<$tsize);
                }
              
                function get_log_subdir() {
                   $suf=($this->log_subdir=='')?'_':'';
                   while ($this->log_subdir=='' || !$this->check_log_subdir()) {
                      $this->log_subdir=$this->gen_log_subdir().$suf.'/';
                   }
                   $lsd=$this->log_dir.$this->log_subdir;
                   if (!file_exists($lsd)) mkdir($lsd);
                   return $this->log_subdir;
                }
              
                function log_subdir() {
                   return $this->log_dir.$this->get_log_subdir();
                }
              
                function print_log($str) {
                  $this->log_file=$this->log_subdir()."daemon.log";
                  $fp = @fopen ($this->log_file, "ab");
                  if (!$fp) return false;
                  if (@flock($fp, LOCK_EX)) {
                    if ($str!='') $str=date("[d-m-y H:i:s] ").'['.$this->log_add.']'.$str;
                    fputs ($fp, $str."\n");
                    @flock($fp, LOCK_UN);
                  } else {
                    @fclose ($fp);
                    return false;
                  }
                  @fclose ($fp);
                  return true;
                }
              

              Привожу их как есть, красиво форматировать «для всех» некогда. Впрочем это просто идея, реализация может быть и другой, более продуманной.
              Смысл этих функций — писать логи, разбивая их на куски.
              То есть мы при старте создаем субдиректорию с человекочитаемой временной меткой и символом '_' в конце имени, пишем в нее логи. Как только размер логов в этой субдиректории достигают заданного размера, мы генерим новую субдиректорию с временной меткой в имени, но уже без '_' в конце, и продолжаем писать логи уже в нее. И так далее.
              Метка '_' в имени ставится только первой субдиректории при старте демона, поэтому глядя в подкаталог логов можно сразу видеть в какое время был старт(рестарт) демона.
              Если нужно найти логи за определенное время, в списке поддиректорий сразу можно понять, в какую надо идти и какой лог смотреть. Это удобнее, чем гигабайтные портянки логов листать.
              Подробные логи всегда дублируются общим неподробным. Туда пишутся только имена пришедших комманд и временные метки. В результате на 100метровую директорию неподробный лог метра четыре весит. По нему быстро можно глянуть, в какое точно время началась активность нужного юзера, а потом уже по этой метке найти в подробном логе все параметры комманд, ответы сервера и т.п.
              Пример листинга директорий с логами
              4EC2691D_15112011_162901_
              4EC67040_18112011_174832
              4ECCC14D_23112011_124757
              4ED24B49_27112011_173801
              4EDD37F5_06122011_003029
              4EE1ACA0_09122011_093720
              4EE71107_13122011_114703
              4EEC5EEC_17122011_122044
              4EF086F2_20122011_160034
              4EF33912_22122011_170506
              4EF73ED1_25122011_181841
              4EF9AC85_27122011_143117
              4EFBBD1F_29122011_040639
              4EFDD4F1_30122011_181249
              4F02CB36_03012012_123238
              4F09DBA9_08012012_210841
              4F0ED584_12012012_154348
              4F15046A_17012012_081730
              4F1A7124_21012012_110244
              4F1FA0B1_25012012_092657
              4F24ECC9_29012012_095257
              4F28E8F9_01022012_102545
              4F2E7DBE_05022012_160150
              4F32BCD4_08022012_212004
              4F3810BF_12022012_221927
              4F3CD2EE_16022012_125702
              4F42007B_20022012_111243
              4F475F9E_24022012_125958
              4F4C8C59_28022012_111209
              4F50E0EB_02032012_180203
              4F55E8A6_06032012_133622
              4F5739F0_07032012_133528_
              4F5754EA_07032012_153034
              4F5B9B13_10032012_211859
              4F5CEAAB_11032012_211051
              4F6192AD_15032012_095645
              ...
              

              Первые 8 HEX-символов это unix timestamp, который далее идет расшифрованным, как [uts_дата_время].
              По меткам '_' видим, что демон стартовал 15.11.2011 в 16:29:01, работал несколько месяцев, был перезапущен 07.03.2012 в 13:35:28 (обновление исходников, поддержка новых комманд).

              В основном коде сервака вызывается только функция print_log(), как-то так:
              $daemon->print_log("alarm! very important tcp-client connected") ;
              
              А там оно само разбирается, в какой лог в какой поддиректории писать.

              В результате(пример из реальных данных) в неподробном логе появляется что-то типа:
              [04-06-12 15:43:00] [     19410][check_logged_in 1CE9020C55E72313CD0EDEF5E3AC3058] :KEY_9
              [04-06-12 15:43:00] [     24273][db_set_value 1CE9020C55E72313CD0EDEF5E3AC30585F1A34060BD5|confirmed|1] :KEY_9 
              

              А в подробном такое(две принятые комманды и отосланные ответы на них):
              Тыц мышей
              [06-04-12 15:43:00] received from 192.168.50.18:3060 [KEY_9]:
              0x00000000  2A 00 00 37 00 00 00 01 00 00 00 00 00 00 00 00  *..7............
              0x00000010  04 00 00 0F 63 68 65 63 6B 5F 6C 6F 67 67 65 64  ....check_logged
              0x00000020  5F 69 6E 04 00 00 20 31 43 45 39 30 32 30 43 35  _in....1CE9020C5
              0x00000030  35 45 37 32 33 31 33 43 44 30 45 44 45 46 35 45  5E72313CD0EDEF5E
              0x00000040  33 41 43 33 30 35 38                             3AC3058
              
              Data:
              array (
                0 => 'check_logged_in',
                1 => '1CE9020C55E72313CD0EDEF5E3AC3058',
              )
              
              
              [06-04-12 15:43:00] sended to 192.168.50.18:3060 [KEY_9]:
              0x00000000  2A 9B 00 0C 00 00 00 01 00 00 00 00 00 00 00 00  *›..............
              0x00000010  02 00 00 02 00 C8 04 00 00 02 4F 6B              .....И....Ok
              
              Data:
              array (
                0 => 200,
                1 => 'Ok',
              )
              
              [06-04-12 15:43:00] received from 192.168.50.18:3060 [KEY_9]:
              0x00000000  2A 00 00 52 00 00 00 01 00 00 00 00 00 00 00 00  *..R............
              0x00000010  04 00 00 0C 64 62 5F 73 65 74 5F 76 61 6C 75 65  ....db_set_value
              0x00000020  04 00 00 2C 31 43 45 39 30 32 30 43 35 35 45 37  ...,1CE9020C55E7
              0x00000030  32 33 31 33 43 44 30 45 44 45 46 35 45 33 41 43  2313CD0EDEF5E3AC
              0x00000040  33 30 35 38 35 46 31 41 33 34 30 36 30 42 44 35  30585F1A34060BD5
              0x00000050  04 00 00 09 63 6F 6E 66 69 72 6D 65 64 04 00 00  ....confirmed...
              0x00000060  01 31                                            .1
              
              Data:
              array (
                0 => 'db_set_value',
                1 => '1CE9020C55E72313CD0EDEF5E3AC30585F1A34060BD5',
                2 => 'confirmed',
                3 => '1',
              )
              
              
              [06-04-12 15:43:00] sended to 192.168.50.18:3060 [KEY_9]:
              0x00000000  2A E3 00 17 00 00 00 01 00 00 00 00 00 00 00 00  *г..............
              0x00000010  02 00 00 02 00 C8 06 00 00 0D 04 00 00 02 49 44  .....И........ID
              0x00000020  02 00 00 03 01 09 98                             ......˜
              
              Data:
              array (
                0 => 200,
                1 => 
                array (
                  'ID' => 67992,
                ),
              ) 
              

              Заголовок пакета — 16 байт. То есть каждая первая строчка HEX-лога.
              Все, что передано в пакете после заголовка, «расшифровывается» сразу за дампом пакета.
  • +1
    А почему все методы объявлены финальными? А если я хочу, например, изменить папку, в которую пишутся логи или имена файлов логов? Или вместо echo «Starting...\n»; пробросить в конструктор логгер и сделать $this->logger->log(«Starting...\n»);?
    • 0
      Для изменения путей к логам есть методы setLog() и setErr()
      • +1
        Вопрос все-таки был «почему все финальное, особенно конструктор». Сеттеры и цепочки методов это конечно хорошо, DI-контейнеры позволяют описать инициализацию один раз и забить, но все-равно, я не вижу смысла лишать этот класс возможности перегрузки методов.
        • 0
          Хм. Возможно вы и правы. А вот с конструктором я наверное просто погорячился.
  • +1
    Мне кажется не очень корректным вывод информации через echo.
    Лучше просто вернуть строку, а что с ней делать уже распорядится тот, кто вызывает функцию.
    • 0
      Какую строку? «Starting...\n» вот эти строки? Они как бы выводятся на консоль и отображают процесс запуска/остановки демона, ну и естественно выводят статус. Я думаю логично, если человек запрашивает php run.php status вывести ему информацию в консоль.
      • +2
        Выводом в консоль заведует метод handle — там по логике и должен быть вывод echo.

        Как то так:
        final public function handle($argv) {
                switch ($argv[1]) {
                    case 'start':
                        echo $this->start();
                        break;
                    case 'stop':
                        echo $this->stop();
                        break;
        [...]
        
        • 0
          Кстати да. Хорошая мысль. Нужно так и сделать. Благодарю.
  • 0
    Забавно, хабрапарсер съел @-теги в докблоках, заменив их на разметку .
    • 0
      … разметку <hh user="">
    • 0
      Ага. Даже не знаю как исправить теперь.
      • 0
        Наверное так: @param
        & # 64; без пробелов
        • 0
          Благодарю. Помогло. Надо будет запомнить на будущее.
  • +1
    И еще \n было бы неплохо заменить на PHP_EOL
    • 0
      Тоже над этим думал. Руки не дошли. Сделал.
    • 0
      а не подскажите для чего?
      • +1
        В разных ОС символ конца строки разный. Если не ошибаюсь, в linux это \n, в macos \r, в windows \r\n.
        • 0
          Я угадал ваш возраст по комментарию! )
          \r в макос было до версии 9, в mac os x уже тоже \n
  • +1
    есть еще нечто подобное phpdaemon.net/
  • –4
    Меня одного коробит при использовании слова «демон» в контексте программы? Неужели не существует более удачного перевода этого термина?
    • +7
      Устоявшийся термин еще со времен первых Юниксов.
      • –1
        Я знаю, но меня все ровно коробит. Хотя отрицать то, что PHP связано с демонами и прочей нечистой силой я не буду.
        • +1
          Ну да, сотрудникам патриархии стоит воздержаться от просмотра прав на файлы в бинарном представлении в юникс-системах:)
    • +2
      А про «зомби» слышали?
      • +2
        Ага. Юникс кишит «зомби», «демонами» и прочей нечестью :)
    • 0
      Суть в том, что это название в *nix системах восходит еще к масвелловскому демону, так что перевод адекватен
  • +1
    У меня используется такой же класс для демонизации. Ну понятно, что отличия есть, но реализовано все то же самое.
    Вот статус проверять по-моему лучше, не только смотря файл с пидом, но и «пингуя» сам процесс:
    У меня это сделано так:
      function check() {
         $pid = $this->read_pid();
         if ($pid>0 && posix_kill($pid, 0)) return true;
         return false;
      }
    


    И еще КМК лучше реализовать реакцию на сигналы, приблизительно так:
      function sig_handler($signo) {
           $func=$this->term_func;
           switch ($signo) {                   
               case SIGTERM:
                   // handle shutdown tasks
                   $this->daemon_del_pid();
                   exit;
                   break;
               case SIGHUP:
                   // handle restart tasks
                   break;
               default:
                   // handle all other signals
           }
           if ($func) $func($signo);
      }
    

    И в методе demonize() уже где-то после второго posix_setsid() привязать сигналы приблизительно таким образом:
    for ($i=1;$i<=SIGTERM;$i++) @pcntl_signal($i, SIG_IGN);
    


    Здесь $this->term_func это поинтер на внешний обработчик сигналов. Т.к. у вас внешняя main() реализована переопределением метода run(), то вы так же можете и этот обработчик сигналов реализовать по-своему.
    • 0
      Обработку сигналов я тоже думаю сделать. На днях поковыряюсь.
      • 0
        Кстати у меня вопрос: вы проверяли работу переназначения STDOUT и STDERR?
        У меня почему-то оно странно работает.
        printf() попадает в stdout лог файл, как положено.
        fprintf(STDERR, ...) валит notice в тот же stdout лог файл, что мол дескриптор 3 недоступен.
        Такое ощущение, что STDERR не хочет так переназначаться или захватывает не тот дескриптор и двигло валит ошибку в stdout из-за недоступности STDERR.
        • 0
          Странно. STDOUT проверял, а вот STDERR что-то и правда не работает.
        • 0
          Сделал набросок для теста… В STDERR отказывается писать.
          • 0
            Я тоже гонял это в разных вариантах. Не хочет и все тут.
            Ну забил и юзаю только STDOUT-файл — ворнинги если есть, туда же валятся.
            Да и тем более это крайняя редкость, сам код демона все-равно в отдельные логи все что нужно пишет.
            Но тем не менее надо помнить — если уж STDOUT/STDERR закрыли, то надо с этим что-то делать. В закрытом виде бросать нельзя.
            Иначе, если у вас в демоне сервер, он начнет акцептить клиентов и первые соединения попадут на дескрипторы STDERR/STDOUT, и тогда любой ворнинг может клиенту в сокет уйти вместо STDERR.
        • 0
          Через fprintf() записать в STDOUT тоже не получается, а вот printf() и echo() пишутся в него. Пробовал уже в разных режимах открывать — не получается.
          • 0
            А, забейте, это внутренняя кухня PHP балуется )
            Константы STDOUT/STDERR остались 2 и 3 соответственно, но связанные с ними дескрипторы ресурсов(видимые из скрипта) захлопнулись. Вот fprintf и не хочет в числовые дескрипторы писать. Ему объект ресурса подавай.
            Связать опять дескрипторы с этими константами мы не можем — это ж мало того, что константы, так еще и системные.
            • 0
              Можно расширить класс и добавить что-то вроде put_log() и put_err(), чтобы облегчить жизнь, но с другой стороны, в метод run() ведь может просто запускаться метод стороннего класса (у меня так и есть), а сторонний класс переписывать уже не хорошо.
              • 0
                Зачем? Смысл-то переназначения STDERR как раз в том, чтобы ошибки и ворнинги самого PHP туда валились. Добавляя put_err(), вы просто плодите функции print_log().
                Ну я выше писал, я забил на этот глюк. Переназначение-то делаю, чтоб сокетам работать не мешало, но рельно этими файлами не пользуюсь.
                • 0
                  P.S. ну еще можно error_reporting функции заюзать и отлавливать PHP-шные сообщения там и писать в файл. Тже метод, если нужно для отладки. Критические ошибки парсинга он при попытке запуска в консоль вывалит, остальные будут писаться куда надо.
                  • 0
                    Не делайте так. Есть ровно одно место, в которое сходятся все «грязные» логи ошибок: это stdout. Вот именно на этом уровне и надо осуществлять перенаправление, именно эта точка и есть «точка принятия решения с максимальной ответственностью».
                    • 0
                      «Грязные» — это какие? Какие логи ошибок лезут в STDOUT и которые нельзя при этом перехватить через хандлеры в set_error_handler() и set_exception_handler()?
                      PHP делался для веба, и в частности CLI-версия предполагает, что при запуске из-под сервака в CGI-режиме лишнее в STDOUT валиться не должно.
                      Поэтому указанными выше хандлерами можно перехватить почти все, кроме критических ошибок на этапе предкомпиляции скрипта — но тут скрипт и не запустится вообще.
                      P.S. под error_reporting функциями я имел в виду именно набор функций работы с error_reporting, а не саму функцию error_reporting().
                      • 0
                        — Ошибки, которые вывели вызываемые через system (или другие средства) внешние утилиты.
                        — Всякие системные сообщения, например, «core dumped», «segmentation fault» и т.д.
                        — Parse error и Fatal error в подключаемых файлах (например, в шаблонах).
                        Мой вам совет — переставайте уже ставить карандаш на острие, у него есть обратная плоская сторона, на которой он замечательно и устойчиво может стоять. :-)
                        • 0
                          Внешние утилиты и их вывод в консоль при надобности контролируются пайпами.
                          Шаблоны и прочую лабуду я динамически к демонам не подключаю. Предпочитаю не создавать себе проблем заранее. Шаблоны если нужны — их можно вместо прямого подключения парсить. Тут вам выбор и раздолье: хоть кусками фиксированной длины через парсер прогоняйте, чтоб лишнюю память не жрать. Как реализовать правильный парсер — отдельная тема. А то вы мне опять тут сейчас скажете «низя не в коем случае» ))
                          «core dumped» и «segmentation fault» у меня такая редкость, что можно если что в режиме консоли запустить и погонять.

                          У меня все и так замечательно и устойчиво работает. Кроном я простые скрипты запускаю, которые и нужно запускать кроном. Но крону — кроново, а демону — демоново. Накой мне крон, когда мне нужен демон?
    • +1
      Для работы pcntl_signal нужно делать declare(ticks=1), а оно deprecated.
      • 0
        А где указано, что deprecated? Не знал.
        Я правда сигналами и не пользуюсь, завязываю все их на SIG_IGN, а демоны между собой общаются по TCP по своему протоколу. Так вроде глюков нет, все пучком.
      • +4
        Тут дело даже не столько в том, что оно deprecated, сколько в механизме, как в интерпретируемых языках (что в PHP, что в Perl) вообще работают сигналы. А работают они… э-эээ… плохо. Когда вы пишете declare(ticks=1), вы заставляете интерпретатор через каждую минимальную опкод-инструкцию вызывать внутренний код проверки состояния (например, какие сигналы за последний квант времени накопились и, если они есть, выполнить обработчики). Такое поведение объясняется тем, что интерпретатор не является повторно входимым, поэтому он не может выполнить код обработчика непосредственно в момент возникновения сигнала — ведь он в этот момент может заниматься обработкой произвольной инструкции. (Честно сказать, та же самая проблема существует и в C++-программах: в обработчике сигнала нельзя выполнять сложные действия, нельзя даже память выделять новую malloc-ом — а значит, нельзя использовать string, vector и другие классы STL.)

        У такого метода с declare, таким образом, 2 недостатка: во-первых, дополнительные тормоза при работе, а во-вторых, вы ведь можете и не попасть на следующую инструкцию вовсе (может зависнуть операция обмена данными с TCP-сокетами, в CURL это происходит периодически, и CURL-овые тайм-ауты тут не помогают даже). Так что сигнал так и останется необработанным.

        Всего есть 2 разумных и безопасных действия, которые можно выполнить в обработчике сигнала:
        1. Убить себя об стену.
        2. Поставить флаг «сделать что-то», который проверяется периодически где-то в основном коде.

        В PHP случай 2 — под вопросом, т.к. требует declare(ticks), а по нему см. недостатки выше. Так что оба этих варианта приемлемо работают только в неинтерпретируемых языках.
        • 0
          скажите, насколько улучшит ситуацию использование функиции pcntl_signal_dispatch() вместо declare(ticks=N)?
          • 0
            Ну тормоза это уберет, конечно. (Фактически, pcntl_signal_dispatch() — это то же самое, что п. 2 в предыдущем комментарии.) Но вот только где гарантия, что скрипт дойдет до точки вызова pcntl_signal_dispatch(), не зависнув по дороге…
            • 0
              если использовать блокируемые соединения это 99,99% что не дойдет
      • +1
        по этому я использую libevent:
        и обработка сигналов
        и таймер
        и TCP общение (клиент и сервер ) в обном флаконе
  • 0
    неплохо написано. Жаль, что такого класса нет в стандартной библиотеке РНР.
    А вы на утечку памяти демоны с использованием этого класса проверяли?
    • 0
      У меня на этом классе работает серверная часть, которая работает с данными в БД. Утечек не наблюдается или они таки ничтожны.
    • 0
      текут в основном системные либы, используемые в экстеншенах,
      У меня демон неделями висит без перезагрузки и мониторинг показывает ровную прямую,
      так что, используем те либы, которые не текут.
  • 0
    System_Daemon ещё есть, например.
  • 0
    Я написал неблольшую обвязку для фреймворка Yii, чтобы демонизировать консольные команды при помощи вашего класса. Если интересно, я мог бы скинуть вам, и вы выложили бы в примеры использования.
    • 0
      Вообще интересно. Можно было бы добавить в документацию на репозитории. Скидывайте на почту.
  • +6
    Когда возникает потребность в постоянно работающем скрипте, как правило, обращают внимание на следующие аспекты:
    1) Скрипт должен сопротивляться одновременному запуску себя в нескольких процессах (чтобы если кто-то дважды запустил демон, во второй раз он получил «я уже работаю, пропущено»).
    2) Скрипт должен сам себя перезапускать через определенное число итераций (чтобы бороться с утечками памяти, которые рано или поздно все равно проявятся).
    3) Скрипт должен уметь запоминать свое текущее состояние, чтобы после внезапной смерти (или перезагрузке, или вылете при превышении лимитов и т.д.) автоматически перезапуститься и продолжить работу примерно с того же места, где в прошлый раз остановился.
    4) Скрипт должен иметь возможность запускать себя «не в режиме демона», для отладки.
    5) Скрипт должен ставить тайм-аут на 1 свою итерацию, чтобы при внезапных подвисаниях вся система не вставала.
    6) Скрипт, выводя что-то в STDOUT и STDERR, должен тем или иным способом снабжать это пометкой текущей даты-времени и PID-а для облегчения отладки.

    По всем этим причинам лично я предпочитаю обходиться вообще без какого-либо кода на PHP для работы с «демонами», а запускать долгоиграющие процессы в кроне. Крон при этом используется не для запуска «по расписанию», а как watchdog — например, скрипт запускается кроном раз в минуту.

    Какие у такого способа преимущества? В основном — вы волей-неволей оказываетесь вынуждены соблюдать все эти правила, иначе ничего не заработает (т.е. это своеобразный самоконтроль), плюс система становится сильно проще. Я прямо по каждому из пунктов выше пройдусь:
    1) Вы волей-неволей должны реализовать и оттестировать защиту от множественного запуска, иначе получите миллион процессов, запущенных кроном.
    2) Самоперезапуск для борьбы с утечками реализуется элементарно — достаточно вставить die(«Перезапускаюсь») в нужное место.
    3) У вас не будет соблазна игнорировать проблему записи текущего состояния, иначе ничего не будет работать.
    4) Т.к. «режима демона» нет, то и запуск в режиме отладки ничем не отличается от запуска в боевом режиме. Вы просто запускаете скрипт в консоли.
    5) Т.к. скрипт готов к смерти в любой момент, то для реализации тайм-аута одной операции вы можете использовать SIGALRM без обработчика — в PHP это приводит к убиванию скрипта, даже если он висит где-то глубоко в IO-операции (весьма удобно).
    6) Вы все равно не сможете в PHP предварять абсолютно любой output маркерами времени (потому что существуют всякие там внешние утилиты, fatal error-ы и т.д.), поэтому гораздо проще запускать скрипт в кроне через «php script.php 2>&1 | logger -t маркер» — это автоматтом даст нужный функционал.

    Ну и самое главное преимущество: вы в любую секунду можете сделать на боевой машине killall php (или даже killall -9 php) и быть уверенным, что ваша система корректно поднимется, и ничего не сломается.
    • +1
      на мой взгляд, много команд в кроне неудобно поддерживать и развертывать на новые сервера. Проще написать такие долгоиграющие демоны, и уже в крон поместить скрипт, который раз в час, например, будет их аккуратно перезапускать.
      Ну а что касательно логов и отладки — соглашусь, несколько усложняются эти пункты.
      • 0
        В крон не надо, конечно, писать много команд, достаточно всего одну, как-то примерно так:

        for s in /path/to/scripts/*.php; do php $s 2>&1 | logger -t $s &; done

        И лучше не раз в час, а раз в минуту — это же watchdog, зачем ждать так долго.
        • +1
          А если в /path/to/scripts/ попадут «левые скрипты»?

          А если в фреймворке, например, Yii, запуск команд делается так:

          yiic commandName params

          то их нужно будет перечислить как-то?

          Поэтому я предпочитаю сделать команду в кроне: yiic runDaemons, в которой написать что-то типа:

          foreach ($app->getDaemons() as $d) {
          runDaemonIfNotRunning($d);
          }

          Это позволяется всё хранить внутри приложения.
          А по поводу часа или минуты — все зависит от того, насколько часто они падают и насколько хорош/плох мониторинг падений. Если мониторинг налажен отлично, то крон и не нужен, по сути.
          • +1
            уточнение: код команды runDaemons находится в приложении, в кроне только запуск.
    • 0
      Кстати говоря, «многопоточные» демоны (в смысле «многопроцессные») ровно через эту же механику очень легко реализуются. Достаточно только поменять код защиты от повторного запуска: например, при запуске выкидывать случайное число от 0 до 9 и блокировать ресурс, имя которого «замешано» на это число (для запуска в 10 потоков). Защиту от повторного запуска проще всего реализовывать через flock (а если нужна работа на нескольких машинах, то можно в случае, например, PostgreSQL делать pg_try_advisory_lock() на мастере).
      • 0
        Вот именно от таких решений я и уходил, когда начал писать «демонов».
        До этого все работало через крон.
    • +2
      Сильно похоже, что вы ломаете свою систему сами, от того и столько требований.

      — Часть требований основана на вашем субъективном восприятии. Нужны вам маркеры времени? Да не вопрос, делаем у демона функцию print_log($str), которая в начало этой $str добавит date("[d-m-y H:i:s] "), в конец добъет "\n" и выплюнет в файл с блокировкой оного flock() на время записи.

      — Утечки памяти? Подвисоны итераций? У меня TCP-сервера на нескольких портах, написанные на PHP, обслуживающие клиентов и при этом еще и общающиеся между собой(перекидывают «неродные» запросы на того, кто его умеет обработать), работают по нескольку месяцев, перезапускаясь только при обновлениях исходников. При этом работают в асинхроне и в одном потоке(thread) одновременно.
      Кстати будет время, вывалю статью и этот класс обработки множества соединений.
      Никаких кронов, ватчдогов и прочего. Никаких утечек, никаких подвисонов. Это все зависит исключительно от программиста. И функционал серверов — не баклуши бить, изредка пописывая что-то в файлик. Конечно и не высоконагруженный проект, но до пары сотен запросов разных в минуту приходит. Помножьте на несколько месяцев. Мемори-лики завалили бы скрипт наверное за пару дней максимум, если бы они были. То есть проблема утечек не в самом PHP явно.

      — Запуск не в режиме демона: Трудно строчку демонизации закомментировать? Или сделать выбор нужного режима параметром коммандной строки?

      Все зависит от задач и от прямоты рук. Иногда лучше кроном регулярно подергивать(особенно простенькие скрипты, которые и отлаживать-то лень), а иногда нужен реальный демон. При этом геморроиться с Сями не всегда охота. Гибкий скриптовый язык удобнее.
      • +1
        Кто-то пишет for ($i = 0; $i != 10; $i++), а кто-то — for ($i = 0; $i < 10; $i++). Вы просто, возможно, из первых (ПОКА из первых?).
        • 0
          Действительно, ПОКА. Вы, как я вижу, оптимист )))
          Не, я вторым способом пишу обычно.
          А иногда и обратный while применяю, где это выгоднее и допустим рекурсивный отсчет:
          $ind=count($arr);
          while ((--$ind)>-1) {
          ...
          }
          

      • 0
        Плюсанул бы, если бы мог. Полностью согласен.
    • 0
      Дмитрий, Вы безусловно правы, и Ваши 6 пунктов — это неотъемлемая часть Правил написания демонов,
      но есть круг задач, в которых скрипт должен быть демоном.

      по этому, часть задач, например, на проверку повторного запуска, у меня делает rcd скрипт

      еще не описана проблема ротации логов: Если демон «захватил» файловый дескриптор, то ротация не отработает,
      а постоянно открывать/закрывать лог — это самоубийство. Ротацию логов: переоткрытие файловых дескрипторов я делаю по сигналу. Сигнал выдаю из prerotate: logrotate.

      самопроверку реализую через таймеры
      • 0
        и еще через таймеры реализую статистику и мониторинг здоровья
        отдаю данные в систему мониторинга через административный порт
        так как система построена на либэвент, то по приходу пакета сбора статистики, отрабатывает скрипт отдачи статистической информации.

        что мониторится:
        — состояние соединений
        — кол-во обработанных элементов (в зависимости от задачи)
        — время простоя (в процентах от общего времени)
        — скорость обработки (кол-во запросов в сек)

      • 0
        Ротация логов сама собой происходит, т.к. логи в моем случае пишутся через стандартный logger (и могут, таким образом, ротироваться через logrotate).

        Наверное, я немного неточно все же выразился: скрипт, запускаемый из крона, точно такой же демон. То, что он запускается снова и снова раз в минуту, вовсе не означает, что он должен работать не больше минуты! Он вполне может работать часами (при этом повторные его запуски будут отсекаться кодом контроля повторного запуска). Т.е. по сути — разницы нет особой.
        • 0
          да, я в одном проекте использовал такую схему:
          — скрипт запускался по крону раз в минуту, если существовал пид — то скрипт завершался (контроль запуска)
          — скрипт перезапускал сам себя раз в 5 мин
  • 0
    нужно обязательно реализовать обработку сигналов:
    SIGHUP — перезагрузка конфигурации — рестарт
    SIGTERM — мягкое завершение без потери данных

  • +1
    и еще необходимо написать про rcd скрипт, чтоб можно запускать
    service mydaemon start
    • –1
      до этого не дошел еще, но мысль такая была, да.
      • 0
        а логгирование, а мониторинг????
        • 0
          мониторинг какого плана вы имеете ввиду. и что конкретно логгировать? логи они конечно пишуться, но они реализованы уже непосредственно в исполняемом коде демона — методе run(). А логирование самого демона, я не совсем понимаю, что там логировать. У меня подобный демон работает неделями и не падает. Занимается он только работой с базой (расчеты и отправка электронки), пока что для других целей я демоны не использовал.
          • 0
            мониторинг — что демон работает, основные его параметры по загрузки, параметры соединения, объем аллоцируемой памяти и тд…

            логгирование работы демона: например сколько писем оправил, сколько из них не дошло, разного рода ошибки.
            • 0
              Такое логирование сделанно в самом коде, который исполняется в методе run(), здесь же приведен всего лишь небольшой пример использования.
              • 0
                а ротация — или лог будет бесконечно увеличиваться?
                • 0
                  Это уже на совести того, кто будет писать код в методе run(). Хотите ротацию — делайте. Этот класс лишь запускает ваш код в режиме демона.
                  • 0
                    Это не совсем верно,
                    раз мы запускаем демона, мы должны продумать все детали, связанные с его запуском.

                    у нас из-за отсутствия ротации однажды лог съел все пространство :(
                    правда в лог постоянно сыпалась ошибка и лог быстро вырос до гигантских размеров.
  • +2
    Не вздумайте пользоваться этим кодом, здесь проверка работы демона осуществляется по существованию файла. Если сервер перезагрузился, процесс аварийно завершился, упал php или еще один из миллиона вариантов катастроф, но при этом файл не был удален, то просто напросто новая копия демона не запуститься. это не теория, сам сталкивался с этим.
    Автор, перепиши проверку работы демона на posix функции.а до этого лучше скрой статью.
    • +1
      Эту ситуацию я уже исправил.
      • 0
        Спасибо. Уже лучше. Но я имел ввиду немного другое.
        При запуске скрипта нужно где-нибудь сделать отметку о том что процесс запущен: в базу или файл записываем pid процесса (получить можно так getmypid()). По окончании всех действий удаляем запись с нашим pid (в случае демона этого делать не надо, ведь мы считаем, что этот процесс всегда должен быть запущен).

        При новом запуске скрипта, например по крону, смотрим в базу/файл на наличие записи о запущенном процессе. Если записи нет, то тогда спокойно запускаемся. Если же запись есть, то нужно проверить, действительно ли такой процесс запущен. Это можно сделать так: (bool) posix_getsid($pid); Ну и дальше действовать по ситуации.

        Еще будет хорошо если рядом с отметкой о запуске с pid процесса будет timestamp запуска. Его можно использовать для разных целей. Например, убивать процесс, который длится больше недели. Так, на всякий случай, для борьбы с утечками и подвисаниями.

        Минус этого способа наличие ничтожно малой вероятность неуникальности pid. Т.е. процесс завершился аварийно, не успев удалить отметку о запуске. А какому-то другому процессу присвоен этот самый pid. В итоге мы посылаем kill другому ни в чем неповинному процессу… Хотя по-моему этим же грешит и Ваш код. Как выход вместо posix_getsid() использовать exec('ps'); с дополнительными параметрами и смотреть не только pid, но еще и имя запущенного процесса.

        Отписывайте в личку, если нужна помощь по доработке кода.
        • 0
          Так у меня проверка и так выполняется. В PID-файл записывается PID процесса. Запустить второй раз его не получится, он скажет «Process is running on PID: XXXXX»
          • 0
            Спасибо, не знал что posix_kill($pid, 0) так работает.
        • 0
          Кстати, про таймштамп хорошая идея…
    • 0
      Если сервер перезагрузился, процесс аварийно завершился, упал php или еще один из миллиона вариантов катастроф, но при этом файл не был удален, то просто напросто новая копия демона не запуститься. это не теория, сам сталкивался с этим.
      знакомая ситуация, существование pid проверяем из /proc/
  • 0
    Добавил еще проверку function_exists('chroot') в setChroot(). А то у меня в блоге отписался человек, у которого старая версия PHP и нет функции chroot()
  • 0
    Можно уточнить, а чем phpDaemon хуже вашей реализации демона?

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