_nParentPid = posix_getpid(); $this->_hShm = shm_attach(mt_rand(), self::SHM_SIZE); if ($this->_hShm === false) { throw new ApnsPHP_Push_Server_Exception( 'Unable to get shared memory segment' ); } $this->_hSem = sem_get(mt_rand()); if ($this->_hSem === false) { throw new ApnsPHP_Push_Server_Exception( 'Unable to get semaphore id' ); } register_shutdown_function(array($this, 'onShutdown')); pcntl_signal(SIGCHLD, array($this, 'onChildExited')); foreach(array(SIGTERM, SIGQUIT, SIGINT) as $nSignal) { pcntl_signal($nSignal, array($this, 'onSignal')); } } /** * Checks if the server is running and calls signal handlers for pending signals. * * Example: * @code * while ($Server->run()) { * // do somethings... * usleep(200000); * } * @endcode * * @return @type boolean True if the server is running. */ public function run() { pcntl_signal_dispatch(); return $this->_nRunningProcesses > 0; } /** * Waits until a forked process has exited and decreases the current running * process number. */ public function onChildExited() { while (pcntl_waitpid(-1, $nStatus, WNOHANG) > 0) { $this->_nRunningProcesses--; } } /** * When a child (not the parent) receive a signal of type TERM, QUIT or INT * exits from the current process and decreases the current running process number. * * @param $nSignal @type integer Signal number. */ public function onSignal($nSignal) { switch ($nSignal) { case SIGTERM: case SIGQUIT: case SIGINT: if (($nPid = posix_getpid()) != $this->_nParentPid) { $this->_log("INFO: Child $nPid received signal #{$nSignal}, shutdown..."); $this->_nRunningProcesses--; exit(0); } break; default: $this->_log("INFO: Ignored signal #{$nSignal}."); break; } } /** * When the parent process exits, cleans shared memory and semaphore. * * This is called using 'register_shutdown_function' pattern. * @see http://php.net/register_shutdown_function */ public function onShutdown() { if (posix_getpid() == $this->_nParentPid) { $this->_log('INFO: Parent shutdown, cleaning memory...'); @shm_remove($this->_hShm) && @shm_detach($this->_hShm); @sem_remove($this->_hSem); } } /** * Set the total processes to start, default is 3. * * @param $nProcesses @type integer Processes to start up. */ public function setProcesses($nProcesses) { $nProcesses = (int)$nProcesses; if ($nProcesses <= 0) { return; } $this->_nProcesses = $nProcesses; } /** * Starts the server forking all processes and return immediately. * * Every forked process is connected to Apple Push Notification Service on start * and enter on the main loop. */ public function start() { for ($i = 0; $i < $this->_nProcesses; $i++) { $this->_nCurrentProcess = $i; $this->_aPids[$i] = $nPid = pcntl_fork(); if ($nPid == -1) { $this->_log('WARNING: Could not fork'); } else if ($nPid > 0) { // Parent process $this->_log("INFO: Forked process PID {$nPid}"); $this->_nRunningProcesses++; } else { // Child process try { parent::connect(); } catch (ApnsPHP_Exception $e) { $this->_log('ERROR: ' . $e->getMessage() . ', exiting...'); exit(1); } $this->_mainLoop(); parent::disconnect(); exit(0); } } } /** * Adds a message to the inter-process message queue. * * Messages are added to the queues in a round-robin fashion starting from the * first process to the last. * * @param $message @type ApnsPHP_Message The message. */ public function add(ApnsPHP_Message $message) { static $n = 0; if ($n >= $this->_nProcesses) { $n = 0; } sem_acquire($this->_hSem); $aQueue = $this->_getQueue(self::SHM_MESSAGES_QUEUE_KEY_START, $n); $aQueue[] = $message; $this->_setQueue(self::SHM_MESSAGES_QUEUE_KEY_START, $n, $aQueue); sem_release($this->_hSem); $n++; } /** * Returns messages in the message queue. * * When a message is successful sent or reached the maximum retry time is removed * from the message queue and inserted in the Errors container. Use the getErrors() * method to retrive messages with delivery error(s). * * @param $bEmpty @type boolean @optional Empty message queue. * @return @type array Array of messages left on the queue. */ public function getQueue($bEmpty = true) { $aRet = array(); sem_acquire($this->_hSem); for ($i = 0; $i < $this->_nProcesses; $i++) { $aRet = array_merge($aRet, $this->_getQueue(self::SHM_MESSAGES_QUEUE_KEY_START, $i)); if ($bEmpty) { $this->_setQueue(self::SHM_MESSAGES_QUEUE_KEY_START, $i); } } sem_release($this->_hSem); return $aRet; } /** * Returns messages not delivered to the end user because one (or more) error * occurred. * * @param $bEmpty @type boolean @optional Empty message container. * @return @type array Array of messages not delivered because one or more errors * occurred. */ public function getErrors($bEmpty = true) { sem_acquire($this->_hSem); $aRet = $this->_getQueue(self::SHM_ERROR_MESSAGES_QUEUE_KEY); if ($bEmpty) { $this->_setQueue(self::SHM_ERROR_MESSAGES_QUEUE_KEY, 0, array()); } sem_release($this->_hSem); return $aRet; } /** * The process main loop. * * During the main loop: the per-process error queue is read and the common error message * container is populated; the per-process message queue is spooled (message from * this queue is added to ApnsPHP_Push queue and delivered). */ protected function _mainLoop() { while (true) { pcntl_signal_dispatch(); if (posix_getppid() != $this->_nParentPid) { $this->_log("INFO: Parent process {$this->_nParentPid} died unexpectedly, exiting..."); break; } sem_acquire($this->_hSem); $this->_setQueue(self::SHM_ERROR_MESSAGES_QUEUE_KEY, 0, array_merge($this->_getQueue(self::SHM_ERROR_MESSAGES_QUEUE_KEY), parent::getErrors()) ); $aQueue = $this->_getQueue(self::SHM_MESSAGES_QUEUE_KEY_START, $this->_nCurrentProcess); foreach($aQueue as $message) { parent::add($message); } $this->_setQueue(self::SHM_MESSAGES_QUEUE_KEY_START, $this->_nCurrentProcess); sem_release($this->_hSem); $nMessages = count($aQueue); if ($nMessages > 0) { $this->_log('INFO: Process ' . ($this->_nCurrentProcess + 1) . " has {$nMessages} messages, sending..."); parent::send(); } else { usleep(self::MAIN_LOOP_USLEEP); } } } /** * Returns the queue from the shared memory. * * @param $nQueueKey @type integer The key of the queue stored in the shared * memory. * @param $nProcess @type integer @optional The process cardinal number. * @return @type array Array of messages from the queue. */ protected function _getQueue($nQueueKey, $nProcess = 0) { if (!shm_has_var($this->_hShm, $nQueueKey + $nProcess)) { return array(); } return shm_get_var($this->_hShm, $nQueueKey + $nProcess); } /** * Store the queue into the shared memory. * * @param $nQueueKey @type integer The key of the queue to store in the shared * memory. * @param $nProcess @type integer @optional The process cardinal number. * @param $aQueue @type array @optional The queue to store into shared memory. * The default value is an empty array, useful to empty the queue. * @return @type boolean True on success, false otherwise. */ protected function _setQueue($nQueueKey, $nProcess = 0, $aQueue = array()) { if (!is_array($aQueue)) { $aQueue = array(); } return shm_put_var($this->_hShm, $nQueueKey + $nProcess, $aQueue); } }