Location: PHPKode > projects > crVCL PHP Framework > daemon.lib.php
<?PHP

/*

  The contents of this file are subject to the Mozilla Public License
  Version 1.1 (the "License"); you may not use this file except in compliance
  with the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL/MPL-1.1.html or see MPL-1.1.txt in directory "license"

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
  the specific language governing rights and limitations under the License.

  The Initial Developers of the Original Code are:
  Copyright (c) 2003-2012, CR-Solutions (http://www.cr-solutions.net), Ricardo Cescon
  All Rights Reserved.

  Contributor(s): PanterMedia GmbH (http://wwwpanthermedianet), Peter Ammel, Ricardo Cescon

  crVCL PHP Framework Version 2.4
 */




############################################################
if (!defined("DAEMON_LIB")) {
   define("DAEMON_LIB", 1);
############################################################
   /**
    * PHP CLI daemon class, require the PCNTL librarie and the POSIX librarie
    *
    */

   class daemon {

      private $m_pid_file = "";
      private $m_started = null;
      private $m_pid = null;
      private $m_shm = null;
      private $m_egid = null;
      private $m_euid = null;
      private $m_isChild = false;
      private $m_killZombieChildsAfter = null;
      public $m_log_file = 'daemon.lib.log';
      public $m_log_path = '';
      public $m_log_delAfter = "1M";

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * create a CLI daemon (basic class for a CLI daemon), for more see the samples
       *
       * @param string $pid_file
       * @param int $mem
       * @param int $egid
       * @param int $euid
       * @param int $killZombieChildsAfter
       */
      function __construct($pid_file, $mem = null, $egid = null, $euid = null, $killZombieChildsAfter = 10) {
         register_shutdown_function(array(&$this, 'catchFatalError'));

         if (!CLI) {
            showFrameworkError("class 'daemon' could be used only in CLI mode");
         }

         if (substr(PHP_OS, 0, 3) === 'WIN') {
            showFrameworkError("class 'daemon' could be not used on Windows");
         }

         if (!function_exists('posix_getpid')) {
            showFrameworkError("PHP is compiled without --enable-posix directive");
         }

         if (function_exists('gc_enable')) {
            gc_enable();
         }

         if (!function_exists('pcntl_signal_dispatch')) {
            showFrameworkError("PHP >= 5.3.0 is required");
         }

         $this->m_killZombieChildsAfter = $killZombieChildsAfter;

         pcntl_signal_dispatch();
         pcntl_signal(SIGINT, array($this, "signalHandlerParent"), false);
         pcntl_signal(SIGTERM, array($this, "signalHandlerParent"), false);
         pcntl_signal(SIGQUIT, array($this, "signalHandlerParent"), false);
         pcntl_signal(SIGHUP, array($this, "signalHandlerParent"), false);
         pcntl_signal(SIGUSR1, array($this, "signalHandlerParent"), false);

         $this->m_started = time();

         if ($mem !== null) {
            $mem = str2byteInt($mem);
         }

         $this->m_egid = $egid;
         $this->m_euid = $euid;

         $this->m_pid = posix_getpid();


         if (strpos($pid_file, '/') === false) {
            if (!isset($_SERVER['SCRIPT_NAME'])) {
               throw new Exception("error get script directory", 0);
            }
            $script_path = dirname($_SERVER['SCRIPT_NAME']);
            $script_path = fixpath($script_path);
            $pid_file = $script_path . '/' . $pid_file;
         }

         if (extractFileExt($pid_file) != "pid") {
            $pid_file = $pid_file . ".pid";
         }

         $this->m_pid_file = $pid_file;

         if (is_file($pid_file)) {
            $pid = @file_get_contents($pid_file);
            throw new Exception("daemon is still running with pid " . $pid . " (" . $pid_file . ")" . BR . BR, 0);
         }

         file_put_contents($pid_file, $this->m_pid);

         // create SHM for IPC
         $this->m_shm = new shm(md5(uniqid(rand())), $mem, true);

         // init internal vars for the daemon
         $this->setIPC("crVCL_Daemon", array("childs" => array(), "requestedStop" => false, "cnames" => array()));

         if ($this->m_log_path == '') {
            $this->m_log_path = dirname($_SERVER['SCRIPT_NAME']);
         }
      }
//-------------------------------------------------------------------------------------------------------------------------------------
      function catchFatalError() {
         $error = error_get_last();
         $ignore = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_STRICT | E_DEPRECATED | E_USER_DEPRECATED;
         if (($error['type'] & $ignore) == 0){
            exit(1);
         }
      }
//-------------------------------------------------------------------------------------------------------------------------------------      
      protected function log($msg, $autolock = true) {
         $path = $this->m_log_path;
         $file = $this->m_log_file;

         if ($path != '' && $file != '' && ($path == '.' || is_dir($path))) {
            $path = fixpath($path);

            $msg = date2MySQLDate() . '.' . strzero(strrcut(microtime(true), '.', true), 4) . ' - ' . $msg . BR;

            if ($autolock)
               $this->lockIPC();


            $fbuf = @file_get_contents($path . "/" . $file);
            $fbuf_len = strlen($fbuf);

            $seperator = CRLF . CRLF . "\t\t\t\t" . CRLF;

            $bytes = str2byteInt($this->m_log_delAfter);
            if (is_file($path . "/" . $file) && @filesize($path . "/" . $file) > $bytes) {
               $fbuf = substr($fbuf, round($fbuf_len / 2, 0));
               $next = strpos($fbuf, $seperator);
               if ($next !== false) {
                  $fbuf = substr($fbuf, $next + strlen($seperator));
               }
            }

            $fbuf .= $msg;

            file_put_contents($path . '/' . $file, $fbuf);

            if ($autolock)
               $this->unlockIPC();
         }
      }

//-------------------------------------------------------------------------------------------------------------------------------------      
      /**
       * required to check in a child fork the daemon(parent) ask for stop
       *
       * @return bool
       */
      public function isRequestedStop() {
         $this->lockIPC();
         $a = $this->getIPC("crVCL_Daemon", false);
         $this->unlockIPC();
         $ret = $a["requestedStop"];
         $a = NULL;
         unset($a);
         gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);
         return $ret;
      }

//-------------------------------------------------------------------------------------------------------------------------------------      
      /**
       * @return shm
       */
      private function &getSHM() {
         return $this->m_shm;
      }

//-------------------------------------------------------------------------------------------------------------------------------------         
      public function signalHandlerParent($signal) {
         // !!! PHP cannot assign a signal handler for SIGKILL (kill -9) !!!
         $this->log('parent signal ' . $signal);
         switch ($signal) {
            case SIGINT:
            case SIGHUP:
            case SIGTERM:   // Shutdown
               $this->stop($this->m_killZombieChildsAfter);
               exit(0);

            case SIGQUIT:
            case SIGCHLD:   // Halt
               $status = null;
               $xpid = null;
               while (($pid = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
                  if ($xpid === null) {
                     $xpid = $pid;
                  }
                  usleep(1000);
               }


               $this->lockIPC();
               $a = $this->getIPC("crVCL_Daemon", false);

               while (list($key, $val) = each($a["childs"])) {
                  if ($xpid == $val) {
                     unset($a["cnames"][$a["childs"][$key]]);
                     unset($a["childs"][$key]);
                     $this->log('child with pid ' . $xpid . ' terminated', false);
                  }
               }

               $a["childs"] = array();
               $this->setIPC("crVCL_Daemon", $a, false);
               $this->unlockIPC();
               $a = NULL;
               unset($a);
               gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);

               break;

            default:   // Ignore all other singals
               break;
         }
      }

//-------------------------------------------------------------------------------------------------------------------------------------      
      public function signalHandlerChild($signal) {
         $this->log('child signal ' . $signal);
         // !!! PHP cannot assign a signal handler for SIGKILL (kill -9) !!!
         switch ($signal) {
            case SIGINT:
            case SIGHUP:
            case SIGTERM:
            case SIGQUIT:
            case SIGCHLD:
            case SIGUSR1:  // Shutdown
               exit(0);
            default:   // Ignore all other singals
               break;
         }
      }

//-------------------------------------------------------------------------------------------------------------------------------------      
      /**
       * set stop flag in the Inter-process communication to give childs the chance to stop self before the parent kill his childs, see also the method "isRequestedStop"
       *
       * @param int $forceAfter_sec null to disable, kill childs after x sec., if they not stops self and run as zombie
       */
      function stop($forceAfter_sec = 5) {
         $this->lockIPC();
         $this->log('request stop childs', false);

         $a = $this->getIPC("crVCL_Daemon", false);

         $a["requestedStop"] = true; // ask childs to stopp

         $this->setIPC("crVCL_Daemon", $a, false);
         $this->unlockIPC();
         mssleep(100);
         $a = NULL;
         unset($a);
         ##############################

         if ($forceAfter_sec !== null) { // force kill zombie childs after timeout
            $childs_running = acount($this->getChild_Pids());

            $wait = $forceAfter_sec * 1000;
            while ($childs_running > 0 && $wait > 0) { // wait till childs stopping self
               mssleep(100);
               $childs_running = acount($this->getChild_Pids());
               $wait = $wait - 100;
            }

            gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);

            if ($childs_running > 0) { // kill zombie childs
               $this->destroyChilds();
            }
         }
      }

//-------------------------------------------------------------------------------------------------------------------------------------      
      /**
       * kill all child processes
       *
       * @param int $pid to kill a certain child, null for all
       *
       */
      function destroyChilds($pid = null) {
         $this->lockIPC();
         $a = $this->getIPC("crVCL_Daemon", false);

         reset($a["childs"]);
         while (list($key, $val) = each($a["childs"])) {
            if ($pid === null || $pid == $val) {
               $status = null;
               $check = pcntl_waitpid($val, $status, WNOHANG OR WUNTRACED);
               switch ($check) {
                  case $pid:
                     //ended successfully
                     break;
                  case 0:
                  case -1:
                  default:
                     $this->log('kill child with pid ' . $child_id, false);
                     posix_kill($child_id, SIGKILL);
               }
               unset($a["cnames"][$a["childs"][$key]]);
               unset($a["childs"][$key]);
            }
         }

         $a["childs"] = array();
         $this->setIPC("crVCL_Daemon", $a, false);
         $this->unlockIPC();
         $a = NULL;
         unset($a);
         gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);
      }

//-------------------------------------------------------------------------------------------------------------------------------------      
      function __destruct() {
         error_reporting(0); // avoid the following Exception from PHP Garbage Collector "Fatal error: Exception thrown without a stack frame in Unknown on line 0"


         if (!$this->m_isChild) {
            $this->destroyChilds();
            if (is_file($this->m_pid_file)) {
               unlink($this->m_pid_file);
            }

            $this->log('end daemon parent' . BR);
         }
      }

//-------------------------------------------------------------------------------------------------------------------------------------      
      /**
       * fork a new process (child), on error a exception will be created
       *
       * @param string $name method or process filename
       * @return bool
       */
      public function fork($name) {
         $fork_num = 0;
         $child_md5 = md5($name) . '_' . strzero($fork_num, 3);

         $pid = pcntl_fork();

         if ($pid === -1) {
            throw new Exception("can't fork " . $name);
         } else if ($pid) { // parent
            $this->lockIPC();
            $a = $this->getIPC("crVCL_Daemon", false);
            $a["requestedStop"] = false;

            if (acount($a["childs"]) == 0) {
               $this->log('daemon started with parent pid ' . $this->m_pid . ' (' . $this->m_pid_file . ')', false);
            }

            $this->log('start child with pid ' . $pid . ' (' . $name . ')', false);

            while (isset($a["childs"][$child_md5])) { // if same is forked more then one time
               $fork_num++;
               $child_md5 = md5($name) . '_' . strzero($fork_num, 3);
            }

            $a["childs"][$child_md5] = $pid; // add new child id
            $a["cnames"][$pid] = $name;
            $this->setIPC("crVCL_Daemon", $a, false);
            $this->unlockIPC();
            $a = NULL;
            unset($a);
            gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);

            $status = null; // fix defunct Zombies
            while (($pid = pcntl_wait($status, WNOHANG OR WUNTRACED)) > -1) {
               if (pcntl_wifexited($status)) {
                  break;
               }
               mssleep(100);
            }
         } else { // child
            mssleep(1000); //  to avoid problems
            $child_id = posix_getpid();
            try {

               $shm = $this->getSHM();
               $shm->setAutoFree(false);

               $this->m_isChild = true;


               pcntl_signal_dispatch();
               pcntl_signal(SIGINT, array($this, "signalHandlerChild"), false);
               pcntl_signal(SIGTERM, array($this, "signalHandlerChild"), false);
               pcntl_signal(SIGQUIT, array($this, "signalHandlerChild"), false);
               pcntl_signal(SIGHUP, array($this, "signalHandlerChild"), false);
               pcntl_signal(SIGUSR1, array($this, "signalHandlerChild"), false);
               pcntl_signal(SIGCHLD, array($this, "signalHandlerChild"), false);


               ####################################

               if ($this->m_egid !== null && !posix_setegid($this->m_egid)) {
                  throw new Exception("could not change effective gid to " . $this->m_egid, 0);
               }
               if ($this->m_euid !== null && !posix_seteuid($this->m_euid)) {
                  throw new Exception("could not change effective uid to " . $this->m_euid, 0);
               }

               ####################################


               if (method_exists($this, $name)) { // fork method
                  //call_user_func(array(&$this, $name));
                  $this->{$name}(); // like "call_user_func" but faster
               } else if (is_file($name)) { // fork thread file
                  require($name);
               }


               // if child is finished, remove from running childs and exit fork
               $this->log('exit child with pid ' . $child_id);

               $this->lockIPC();
               $a = $this->getIPC("crVCL_Daemon", false);

               $key = array_search($child_id, $a["childs"]);
               if ($key !== false) { // remove from running childs
                  unset($a["cnames"][$a["childs"][$key]]);
                  unset($a["childs"][$key]);
               }

               $this->setIPC("crVCL_Daemon", $a, false);
               $this->unlockIPC();

               $a = NULL;
               unset($a);
               gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);

               exit(0);
               #return true;
            } catch (Exception $e) { // catch zombies
               try {
                  $this->log('child with pid ' . $child_id . ' crashed');

                  $this->lockIPC();
                  $a = $this->getIPC("crVCL_Daemon", false);

                  $status = null;
                  $check = pcntl_waitpid($child_id, $status, WNOHANG OR WUNTRACED);
                  switch ($check) {
                     case $child_id:
                        //ended successfully
                        break;
                     case 0:
                     case -1:
                     default:
                        $key = array_search($child_id, $a["childs"]);
                        if ($key !== false) { // remove from running childs
                           unset($a["cnames"][$a["childs"][$key]]);
                           unset($a["childs"][$key]);
                        }

                        $this->setIPC("crVCL_Daemon", $a, false);
                        $this->unlockIPC();

                        $a = NULL;
                        unset($a);
                        gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);



                        $a = $this->getChild_Names();
                        $clist = '';
                        while (list($key, $val) = each($a)) {
                           $clist .= $key.' (' . $val . ') - ';
                        }
                        $clist = substr($clist, 0, -2);
                        $this->log('current runnig childs: '.$clist, false);

                        // don't change order
                        $this->log('try to kill zombie child with pid ' . $child_id, false);
                        posix_kill($child_id, SIGKILL);
                  }
                  
               } catch (Exception $e2) {
                  $this->log($e2->getMessage().' at Line '.$e2->getLine().LF.$e2->getTraceAsString(), false);
               }

               throw $e;
            }
         }

         return true; // don't remove
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * return the pid of the parent process
       *
       * @return int
       */
      public function getPid() {
         return $this->m_pid;
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * return an array with all running child pids
       *
       * @return array
       */
      public function getChild_Pids() {
         $this->lockIPC();
         $a = $this->getIPC("crVCL_Daemon", false);
         $ret = $a["childs"];
         $this->unlockIPC();
         $a = NULL;
         unset($a);
         gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);
         return $ret;
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * return an array with all running child names
       *
       * @return array
       */
      public function getChild_Names() {
         $this->lockIPC();
         $a = $this->getIPC("crVCL_Daemon", false);
         $ret = $a["cnames"];
         $this->unlockIPC();
         $a = NULL;
         unset($a);
         gc_collect_cycles_overX($GLOBALS['CRVCL']['GC_COLLECT_CYCLES_PERCENT']);
         return $ret;
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * return the uptime of the daemon
       *
       * @return int
       */
      public function getUptime() {
         return (time() - $this->m_started);
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * lock the shared memory for other processes
       *
       */
      function lockIPC() {
         $shm = $this->getSHM();
         $shm->lock();
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * unlock the shared memory for other processes
       *
       */
      function unlockIPC() {
         $shm = $this->getSHM();
         $shm->unlock();
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * get a variable from the SHM (used for Inter-process communication)
       *
       * @param string $varname
       * @param bool $autolock automatic lock/unlock the shared memory for other processes
       * @return mixed
       */
      function getIPC($varname, $autolock = true) {
         $shm = $this->getSHM();
         return $shm->get($varname, $autolock);
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
      /**
       * set a variable to the SHM, if the memory is not unlimited and to small, a exception will be created (used for Inter-process communication)
       *
       * @param string $varname
       * @param mixed $value
       * @param bool $autolock automatic lock/unlock the shared memory for other processes
       * @return bool
       */
      function setIPC($varname, $value, $autolock = true) {
         $shm = $this->getSHM();
         return $shm->set($varname, $value, $autolock);
      }

//-------------------------------------------------------------------------------------------------------------------------------------   
   }

###########################################################
}
############################################################
?>
Return current item: crVCL PHP Framework