PageRenderTime 72ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/Core/Daemon.php

http://github.com/shaneharter/PHP-Daemon
PHP | 1341 lines | 710 code | 190 blank | 441 comment | 142 complexity | fc73b69cfa4f57261aeb961a007668d2 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. declare(ticks = 5);
  3. /**
  4. * Daemon Base Class - Extend this to build daemons.
  5. * @uses PHP 5.3 or Higher
  6. * @author Shane Harter
  7. * @link https://github.com/shaneharter/PHP-Daemon
  8. * @see https://github.com/shaneharter/PHP-Daemon/wiki/Daemon-Startup-Order-Explained
  9. * @singleton
  10. * @abstract
  11. */
  12. abstract class Core_Daemon
  13. {
  14. /**
  15. * The application will attempt to restart itself it encounters a recoverable fatal error after it's been running
  16. * for at least this many seconds. Prevents killing the server with process forking if the error occurs at startup.
  17. * @var integer
  18. */
  19. const MIN_RESTART_SECONDS = 10;
  20. /**
  21. * Events can be attached to each state using the on() method
  22. * @var integer
  23. */
  24. const ON_ERROR = 0; // error() or fatal_error() is called
  25. const ON_SIGNAL = 1; // the daemon has received a signal
  26. const ON_INIT = 2; // the library has completed initialization, your setup() method is about to be called. Note: Not Available to Worker code.
  27. const ON_PREEXECUTE = 3; // inside the event loop, right before your execute() method
  28. const ON_POSTEXECUTE = 4; // and right after
  29. const ON_FORK = 5; // in a background process right after it has been forked from the daemon
  30. const ON_PIDCHANGE = 6; // whenever the pid changes -- in a background process for example
  31. const ON_IDLE = 7; // called when there is idle time at the end of a loop_interval, or at the idle_probability when loop_interval isn't used
  32. const ON_REAP = 8; // notification from the OS that a child process of this application has exited
  33. const ON_SHUTDOWN = 10; // called at the top of the destructor
  34. /**
  35. * An array of instructions that's displayed when the -i param is passed to the application.
  36. * Helps sysadmins and users of your daemons get installation correct. Guide them to set
  37. * correct permissions, supervisor/monit setup, crontab entries, init.d scripts, etc
  38. * @var Array
  39. */
  40. protected $install_instructions = array();
  41. /**
  42. * The frequency of the event loop. In seconds.
  43. *
  44. * In timer-based applications your execute() method will be called every $loop_interval seconds. Any remaining time
  45. * at the end of an event loop iteration will dispatch an ON_IDLE event and your application will sleep(). If the
  46. * event loop takes longer than the $loop_interval an error will be written to your application log.
  47. *
  48. * @example $this->loop_interval = 300; // execute() will be called once every 5 minutes
  49. * @example $this->loop_interval = 0.5; // execute() will be called 2 times every second
  50. * @example $this->loop_interval = 0; // execute() will be called immediately -- There will be no sleep.
  51. *
  52. * @var float The interval in Seconds
  53. */
  54. protected $loop_interval = null;
  55. /**
  56. * Control how often the ON_IDLE event fires in applications that do not use a $loop_interval timer.
  57. *
  58. * The ON_IDLE event gives your application (and the PHP Simple Daemon library) a way to defer work to be run
  59. * when your application has idle time and would normally just sleep(). In timer-based applications that is very
  60. * deterministic. In applications that don't use the $loop_interval timer, this probability factor applied in each
  61. * iteration of the event loop to periodically dispatch ON_IDLE.
  62. *
  63. * Note: This value is completely ignored when using $loop_interval. In those cases, ON_IDLE is fired when there is
  64. * remaining time at the end of your loop.
  65. *
  66. * Note: If you want to take responsibility for dispatching the ON_IDLE event in your application, just set
  67. * this to 0 and dispatch the event periodically, eg:
  68. * $this->dispatch(array(self::ON_IDLE));
  69. *
  70. * @var float The probability, from 0.0 to 1.0.
  71. */
  72. protected $idle_probability = 0.50;
  73. /**
  74. * The frequency of your application restarting itself. In seconds.
  75. *
  76. * @example $this->auto_restart_interval = 3600; // Daemon will be restarted once an hour
  77. * @example $this->auto_restart_interval = 43200; // Daemon will be restarted twice per day
  78. * @example $this->auto_restart_interval = 86400; // Daemon will be restarted once per day
  79. *
  80. * @var integer The interval in Seconds
  81. */
  82. protected $auto_restart_interval = 43200;
  83. /**
  84. * Process ID
  85. * @var integer
  86. */
  87. private $pid;
  88. /**
  89. * Array of worker aliases
  90. * @var Array
  91. */
  92. private $workers = array();
  93. /**
  94. * Array of plugin aliases
  95. * @var Array
  96. */
  97. private $plugins = array();
  98. /**
  99. * Map of callbacks that have been registered using on()
  100. * @var Array
  101. */
  102. private $callbacks = array();
  103. /**
  104. * Runtime statistics for a recent window of execution
  105. * @var Array
  106. */
  107. private $stats = array();
  108. /**
  109. * Dictionary of application-wide environment vars with defaults.
  110. * @see Core_Daemon::set()
  111. * @see Core_Daemon::get()
  112. * @var array
  113. */
  114. private static $env = array(
  115. 'parent' => true,
  116. );
  117. /**
  118. * Handle for log() method,
  119. * @see Core_Daemon::log()
  120. * @see Core_Daemon::restart();
  121. * @var stream
  122. */
  123. private static $log_handle = false;
  124. /**
  125. * Implement this method to define plugins
  126. * @return void
  127. */
  128. protected function setup_plugins()
  129. {
  130. }
  131. /**
  132. * Implement this method to define workers
  133. * @return void
  134. */
  135. protected function setup_workers()
  136. {
  137. }
  138. /**
  139. * The setup method will contain the one-time setup needs of the daemon.
  140. * It will be called as part of the built-in init() method.
  141. * Any exceptions thrown from setup() will be logged as Fatal Errors and result in the daemon shutting down.
  142. * @return void
  143. * @throws Exception
  144. */
  145. abstract protected function setup();
  146. /**
  147. * The execute method will contain the actual function of the daemon.
  148. * It can be called directly if needed but its intention is to be called every iteration by the ->run() method.
  149. * Any exceptions thrown from execute() will be logged as Fatal Errors and result in the daemon attempting to restart or shut down.
  150. *
  151. * @return void
  152. * @throws Exception
  153. */
  154. abstract protected function execute();
  155. /**
  156. * Return a log file name that will be used by the log() method.
  157. *
  158. * You could hard-code a string like '/var/log/myapplog', read an option from an ini file, create a simple log
  159. * rotator using the date() method, etc
  160. *
  161. * Note: This method will be called during startup and periodically afterwards, on even 5-minute intervals: If you
  162. * start your application at 13:01:00, the next check will be at 13:05:00, then 13:10:00, etc. This periodic
  163. * polling enables you to build simple log rotation behavior into your app.
  164. *
  165. * @return string
  166. */
  167. abstract protected function log_file();
  168. /**
  169. * Return an instance of the Core_Daemon singleton
  170. * @return Core_Daemon
  171. */
  172. public static function getInstance()
  173. {
  174. static $o = null;
  175. if (!$o)
  176. try
  177. {
  178. $o = new static;
  179. $o->setup_plugins();
  180. $o->setup_workers();
  181. $o->check_environment();
  182. $o->init();
  183. }
  184. catch (Exception $e)
  185. {
  186. $o->fatal_error($e->getMessage());
  187. }
  188. return $o;
  189. }
  190. /**
  191. * Set an application-wide environment variable
  192. * @param $key
  193. * @param $value
  194. */
  195. protected static function set($key, $value)
  196. {
  197. self::$env[$key] = $value;
  198. }
  199. /**
  200. * Read an application-wide environment variable
  201. * @param $key
  202. * @return Mixed -- Returns Null if the $key does not exist
  203. */
  204. public static function get($key)
  205. {
  206. if (isset(self::$env[$key]))
  207. return self::$env[$key];
  208. return null;
  209. }
  210. /**
  211. * A simple alias for get() to create more semantically-correct and self-documenting code.
  212. * @example Core_Daemon::get('parent') === Core_Daemon::is('parent')
  213. * @param $key
  214. * @return Mixed
  215. */
  216. public static function is($key)
  217. {
  218. return self::get($key);
  219. }
  220. protected function __construct()
  221. {
  222. global $argv;
  223. // We have to set any installation instructions before we call getopt()
  224. $this->install_instructions[] = "Add to Supervisor or Monit, or add a Crontab Entry:\n * * * * * " . $this->command();
  225. $this->set('filename', $argv[0]);
  226. $this->set('start_time', time());
  227. $this->pid(getmypid());
  228. $this->getopt();
  229. }
  230. /**
  231. * Ensure that essential runtime conditions are met.
  232. * To easily add rules to this, overload this method, build yourself an array of error messages,
  233. * and then call parent::check_environment($my_errors)
  234. * @param Array $errors
  235. * @return void
  236. * @throws Exception
  237. */
  238. protected function check_environment(Array $errors = array())
  239. {
  240. if (is_numeric($this->loop_interval) == false)
  241. $errors[] = "Invalid Loop Interval: $this->loop_interval";
  242. if (is_numeric($this->auto_restart_interval) == false)
  243. $errors[] = "Invalid auto-restart interval: $this->auto_restart_interval";
  244. if (is_numeric($this->auto_restart_interval) && $this->auto_restart_interval < self::MIN_RESTART_SECONDS)
  245. $errors[] = 'Auto-restart inteval is too low. Minimum value: ' . self::MIN_RESTART_SECONDS;
  246. if (function_exists('pcntl_fork') == false)
  247. $errors[] = "The PCNTL Extension is not installed";
  248. if (version_compare(PHP_VERSION, '5.3.0') < 0)
  249. $errors[] = "PHP 5.3 or higher is required";
  250. foreach ($this->plugins as $plugin)
  251. foreach ($this->{$plugin}->check_environment() as $error)
  252. $errors[] = "[$plugin] $error";
  253. foreach ($this->workers as $worker)
  254. foreach ($this->{$worker}->check_environment() as $error)
  255. $errors[] = "[$worker] $error";
  256. if (count($errors)) {
  257. $errors = implode("\n ", $errors);
  258. throw new Exception("Checking Dependencies... Failed:\n $errors");
  259. }
  260. }
  261. /**
  262. * Run the setup() methods of installed plugins, installed workers, and the subclass, in that order. And dispatch the ON_INIT event.
  263. * @return void
  264. */
  265. private function init()
  266. {
  267. $signals = array (
  268. // Handled by Core_Daemon:
  269. SIGTERM, SIGINT, SIGUSR1, SIGHUP, SIGCHLD,
  270. // Ignored by Core_Daemon -- register callback ON_SIGNAL to listen for them.
  271. // Some of these are duplicated/aliased, listed here for completeness
  272. SIGUSR2, SIGCONT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGIOT, SIGBUS, SIGFPE, SIGSEGV, SIGPIPE, SIGALRM,
  273. SIGCONT, SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF,
  274. SIGWINCH, SIGIO, SIGSYS, SIGBABY
  275. );
  276. if (defined('SIGPOLL')) $signals[] = SIGPOLL;
  277. if (defined('SIGPWR')) $signals[] = SIGPWR;
  278. if (defined('SIGSTKFLT')) $signals[] = SIGSTKFLT;
  279. foreach(array_unique($signals) as $signal)
  280. pcntl_signal($signal, array($this, 'signal'));
  281. $this->plugin('ProcessManager');
  282. foreach ($this->plugins as $plugin)
  283. $this->{$plugin}->setup();
  284. $this->dispatch(array(self::ON_INIT));
  285. foreach ($this->workers as $worker)
  286. $this->{$worker}->setup();
  287. $this->loop_interval($this->loop_interval);
  288. // Queue any housekeeping tasks we want performed periodically
  289. $this->on(self::ON_IDLE, array($this, 'stats_trim'), (empty($this->loop_interval)) ? null : ($this->loop_interval * 50)); // Throttle to about once every 50 iterations
  290. $this->setup();
  291. if (!$this->is('daemonized'))
  292. $this->log('Note: The daemonize (-d) option was not set: This process is running inside your shell. Auto-Restart feature is disabled.');
  293. $this->log('Application Startup Complete. Starting Event Loop.');
  294. }
  295. /**
  296. * Tear down all plugins and workers and free any remaining resources
  297. * @return void
  298. */
  299. public function __destruct()
  300. {
  301. try
  302. {
  303. $this->set('shutdown', true);
  304. $this->dispatch(array(self::ON_SHUTDOWN));
  305. foreach(array_merge($this->workers, $this->plugins) as $object) {
  306. $this->{$object}->teardown();
  307. unset($this->{$object});
  308. }
  309. }
  310. catch (Exception $e)
  311. {
  312. $this->fatal_error(sprintf('Exception Thrown in Shutdown: %s [file] %s [line] %s%s%s',
  313. $e->getMessage(), $e->getFile(), $e->getLine(), PHP_EOL, $e->getTraceAsString()));
  314. }
  315. $this->callbacks = array();
  316. if ($this->is('parent') && $this->get('pid_file') && file_exists($this->get('pid_file')) && file_get_contents($this->get('pid_file')) == $this->pid)
  317. unlink($this->get('pid_file'));
  318. if ($this->is('parent') && $this->is('stdout'))
  319. echo PHP_EOL;
  320. }
  321. /**
  322. * Some accessors are available as setters only within their defined scope, but can be
  323. * used as getters universally. Filter out setter arguments and proxy the call to the accessor.
  324. * Note: A warning will be raised if the accessor is used as a setter out of scope.
  325. * @example $someDaemon->loop_interval()
  326. *
  327. * @param $method
  328. * @param $args
  329. * @return mixed
  330. */
  331. public function __call($method, $args)
  332. {
  333. $deprecated = array('shutdown', 'verbose', 'is_daemon', 'filename');
  334. if (in_array($method, $deprecated)) {
  335. throw new Exception("Deprecated method call: $method(). Update your code to use the v2.1 get(), set() and is() methods.");
  336. }
  337. $accessors = array('loop_interval', 'pid');
  338. if (in_array($method, $accessors)) {
  339. if ($args)
  340. trigger_error("The '$method' accessor can not be used as a setter in this context. Supplied arguments ignored.", E_USER_WARNING);
  341. return call_user_func_array(array($this, $method), array());
  342. }
  343. // Handle any calls to __invoke()able objects
  344. if (in_array($method, $this->workers))
  345. return call_user_func_array($this->$method, $args);
  346. throw new Exception("Invalid Method Call '$method'");
  347. }
  348. /**
  349. * This is the main program loop for the daemon
  350. * @return void
  351. */
  352. public function run()
  353. {
  354. try
  355. {
  356. while ($this->is('parent') && !$this->is('shutdown'))
  357. {
  358. $this->timer(true);
  359. $this->auto_restart();
  360. $this->dispatch(array(self::ON_PREEXECUTE));
  361. $this->execute();
  362. $this->dispatch(array(self::ON_POSTEXECUTE));
  363. $this->timer();
  364. }
  365. }
  366. catch (Exception $e)
  367. {
  368. $this->fatal_error(sprintf('Uncaught Exception in Event Loop: %s [file] %s [line] %s%s%s',
  369. $e->getMessage(), $e->getFile(), $e->getLine(), PHP_EOL, $e->getTraceAsString()));
  370. }
  371. }
  372. /**
  373. * Register a callback for the given $event. Use the event class constants for built-in events. Add and dispatch
  374. * your own events however you want.
  375. * @param $event mixed scalar When creating custom events, keep ints < 100 reserved for the daemon
  376. * @param $callback closure|callback
  377. * @param $throttle Optional time in seconds to throttle calls to the given $callback. For example, if
  378. * $throttle = 10, the provided $callback will not be called more than once every 10 seconds, even if the
  379. * given $event is dispatched more frequently than that.
  380. * @param $criteria closure|callback Optional. If provided, any event payload will be passed to this callable and
  381. * the event dispatched only if it returns truthy.
  382. * @return array The return value can be passed to off() to unbind the event
  383. * @throws Exception
  384. */
  385. public function on($event, $callback, $throttle = null, $criteria = null)
  386. {
  387. if (!is_scalar($event))
  388. throw new Exception(__METHOD__ . ' Failed. Event type must be Scalar. Given: ' . gettype($event));
  389. if (!isset($this->callbacks[$event]))
  390. $this->callbacks[$event] = array();
  391. $this->callbacks[$event][] = array (
  392. 'callback' => $callback,
  393. 'criteria' => $criteria,
  394. 'throttle' => $throttle,
  395. 'call_at' => 0
  396. );
  397. end($this->callbacks[$event]);
  398. return array($event, key($this->callbacks[$event]));
  399. }
  400. /**
  401. * Remove a callback previously registered with on(). Returns the callback.
  402. * @param array $event Should be the array returned when you called on()
  403. * @return callback|closure|null returns the registered event handler assuming $event is valid
  404. */
  405. public function off(Array $event)
  406. {
  407. if (isset($event[0]) && isset($event[1])) {
  408. $cb = $this->callbacks[$event[0]][$event[1]];
  409. unset($this->callbacks[$event[0]][$event[1]]);
  410. return $cb;
  411. }
  412. return null;
  413. }
  414. /**
  415. * Dispatch callbacks. Can either pass an array referencing a specific callback (eg the return value from an on() call)
  416. * or you can pass it an array with the event type and all registered callbacks will be called.
  417. * @param array $event Either an array with a single item (an event type) or 2
  418. * items (an event type, and a callback ID for that event type)
  419. * @param array $args Array of arguments passed to the event listener
  420. */
  421. public function dispatch(Array $event, Array $args = array())
  422. {
  423. if (!isset($event[0]) || !isset($this->callbacks[$event[0]]))
  424. return;
  425. // A specific callback is being dispatched...
  426. if (isset($event[1]) && isset($this->callbacks[$event[0]][$event[1]])) {
  427. $callback =& $this->callbacks[$event[0]][$event[1]];
  428. if ($callback['throttle'] && time() < $callback['call_at'])
  429. return;
  430. if (is_callable($callback['criteria']) && !$callback['criteria']($args))
  431. return;
  432. $callback['call_at'] = time() + (int)$callback['throttle'];
  433. call_user_func_array($callback['callback'], $args);
  434. return;
  435. }
  436. // All callbacks attached to a given event are being dispatched...
  437. foreach($this->callbacks[$event[0]] as $callback_id => $callback) {
  438. if ($callback['throttle'] && time() < $callback['call_at'])
  439. continue;
  440. if (is_callable($callback['criteria']) && !$callback['criteria']($args))
  441. return;
  442. $this->callbacks[$event[0]][$callback_id]['call_at'] = time() + (int)$callback['throttle'];
  443. call_user_func_array($callback['callback'], $args);
  444. }
  445. }
  446. /**
  447. * Run any task asynchronously by passing it to this method. Will fork into a child process, execute the supplied
  448. * code, and exit.
  449. *
  450. * The $callable provided can be a standard PHP Callback, a Closure, or any object that implements Core_ITask
  451. *
  452. * Note: If the task uses MySQL or certain other outside resources, the connection will have to be
  453. * re-established in the child process. There are three options:
  454. *
  455. * 1. Put any mysql setup or connection code in your task:
  456. * This is not suggested because it's bad design but it's certainly possible.
  457. *
  458. * 2. Run the same setup code in every background task:
  459. * Any event handlers you set using $this->on(ON_FORK) will be run in every forked Task and Worker process
  460. * before the callable is called.
  461. *
  462. * 3. Run setup code specific to the current background task:
  463. * If you need to run specific setup code for a task or worker you have to use an object. You can't use the shortened
  464. * form of passing a callback or closure. For tasks that means an object that implements Core_ITask. For workers,
  465. * it's Core_IWorker. The setup() and teardown() methods defined in the interfaces are natural places to handle
  466. * database connections, etc.
  467. *
  468. * @link https://github.com/shaneharter/PHP-Daemon/wiki/Tasks
  469. *
  470. * @param callable|Core_ITask $callable A valid PHP callback or closure.
  471. * @param Mixed All additional params are passed to the $callable
  472. * @return Core_Lib_Process|boolean Return a newly created Process object or false on failure
  473. */
  474. public function task($task)
  475. {
  476. if ($this->is('shutdown')) {
  477. $this->log("Daemon is shutting down: Cannot run task()");
  478. return false;
  479. }
  480. // Standardize the $task into a $callable
  481. // If a Core_ITask was passed in, wrap it in a closure
  482. // If no group is provided, add the process to an adhoc "tasks" group. A group identifier is required.
  483. // @todo this group thing is not elegant. Improve it.
  484. if ($task instanceof Core_ITask) {
  485. $group = $task->group();
  486. $callable = function() use($task) {
  487. $task->setup();
  488. call_user_func(array($task, 'start'));
  489. $task->teardown();
  490. };
  491. } else {
  492. $group = 'tasks';
  493. $callable = $task;
  494. }
  495. $proc = $this->ProcessManager->fork($group);
  496. if ($proc === false) {
  497. // Parent Process - Fork Failed
  498. $e = new Exception();
  499. $this->error('Task failed: Could not fork.');
  500. $this->error($e->getTraceAsString());
  501. return false;
  502. }
  503. if ($proc === true) {
  504. // Child Process
  505. $this->set('start_time', time());
  506. $this->set('parent', false);
  507. $this->set('parent_pid', $this->pid);
  508. $this->pid(getmypid());
  509. // Remove unused worker objects. They can be memory hogs.
  510. foreach($this->workers as $worker)
  511. if (!is_array($callable) || $callable[0] != $this->{$worker})
  512. unset($this->{$worker});
  513. $this->workers = $this->stats = array();
  514. try {
  515. call_user_func_array($callable, array_slice(func_get_args(), 1));
  516. } catch (Exception $e) {
  517. $this->error('Exception Caught in Task: ' . $e->getMessage());
  518. }
  519. exit;
  520. }
  521. // Parent Process - Return the newly created Core_Lib_Process object
  522. return $proc;
  523. }
  524. /**
  525. * Log the $message to the filename returned by Core_Daemon::log_file() and/or optionally print to stdout.
  526. * Multi-Line messages will be handled nicely.
  527. *
  528. * Note: Your log_file() method will be called every 5 minutes (at even increments, eg 00:05, 00:10, 00:15, etc) to
  529. * allow you to rotate the filename based on time (one log file per month, day, hour, whatever) if you wish.
  530. *
  531. * Note: You may find value in overloading this method in your app in favor of a more fully-featured logging tool
  532. * like log4php or Zend_Log. There are fantastic logging libraries available, and this simplistic home-grown option
  533. * was chosen specifically to avoid forcing another dependency on you.
  534. *
  535. * @param string $message
  536. * @param string $label Truncated at 12 chars
  537. */
  538. public function log($message, $label = '', $indent = 0)
  539. {
  540. static $log_file = '';
  541. static $log_file_check_at = 0;
  542. static $log_file_error = false;
  543. $header = "\nDate PID Label Message\n";
  544. $date = date("Y-m-d H:i:s");
  545. $pid = str_pad($this->pid, 5, " ", STR_PAD_LEFT);
  546. $label = str_pad(substr($label, 0, 12), 13, " ", STR_PAD_RIGHT);
  547. $prefix = "[$date] $pid $label" . str_repeat("\t", $indent);
  548. if (time() >= $log_file_check_at && $this->log_file() != $log_file) {
  549. $log_file = $this->log_file();
  550. $log_file_check_at = mktime(date('H'), (date('i') - (date('i') % 5)) + 5, null);
  551. @fclose(self::$log_handle);
  552. self::$log_handle = $log_file_error = false;
  553. }
  554. if (self::$log_handle === false) {
  555. if (strlen($log_file) > 0 && self::$log_handle = @fopen($log_file, 'a+')) {
  556. if ($this->is('parent')) {
  557. fwrite(self::$log_handle, $header);
  558. if ($this->is('stdout'))
  559. echo $header;
  560. }
  561. } elseif (!$log_file_error) {
  562. $log_file_error = true;
  563. trigger_error(__CLASS__ . "Error: Could not write to logfile " . $log_file, E_USER_WARNING);
  564. }
  565. }
  566. $message = $prefix . ' ' . str_replace("\n", "\n$prefix ", trim($message)) . "\n";
  567. if (self::$log_handle)
  568. fwrite(self::$log_handle, $message);
  569. if ($this->is('stdout'))
  570. echo $message;
  571. }
  572. public function debug($message, $label = '')
  573. {
  574. if (!$this->is('verbose'))
  575. return;
  576. $this->log($message, $label);
  577. }
  578. /**
  579. * Log the provided $message and dispatch an ON_ERROR event.
  580. *
  581. * The library has no concept of a runtime error. If your application doesn't attach any ON_ERROR listeners, there
  582. * is literally no difference between using this and just passing the message to Core_Daemon::log().
  583. *
  584. * @param $message
  585. * @param string $label
  586. */
  587. public function error($message, $label = '')
  588. {
  589. $this->log($message, $label);
  590. $this->dispatch(array(self::ON_ERROR), array($message));
  591. }
  592. /**
  593. * Raise a fatal error and kill-off the process. If it's been running for a while, it'll try to restart itself.
  594. * @param string $message
  595. * @param string $label
  596. */
  597. public function fatal_error($message, $label = '')
  598. {
  599. $this->error($message, $label);
  600. if ($this->is('parent')) {
  601. $this->log(get_class($this) . ' is Shutting Down...');
  602. $delay = 2;
  603. if ($this->is('daemonized') && ($this->runtime() + $delay) > self::MIN_RESTART_SECONDS) {
  604. sleep($delay);
  605. $this->restart();
  606. }
  607. }
  608. // If we get here, it means we couldn't try a re-start or we tried and it just didn't work.
  609. echo PHP_EOL;
  610. exit(1);
  611. }
  612. /**
  613. * When a signal is sent to the process it'll be handled here
  614. * @param integer $signal
  615. * @return void
  616. */
  617. public function signal($signal)
  618. {
  619. switch ($signal)
  620. {
  621. case SIGUSR1:
  622. // kill -10 [pid]
  623. $this->dump();
  624. break;
  625. case SIGHUP:
  626. // kill -1 [pid]
  627. $this->restart();
  628. break;
  629. case SIGINT:
  630. case SIGTERM:
  631. if ($this->is('parent'))
  632. $this->log("Shutdown Signal Received\n");
  633. $this->set('shutdown', true);
  634. break;
  635. }
  636. $this->dispatch(array(self::ON_SIGNAL), array($signal));
  637. }
  638. /**
  639. * Get the fully qualified command used to start (and restart) the daemon
  640. * @param string $options An options string to use in place of whatever options were present when the daemon was started.
  641. * @return string
  642. */
  643. private function command($options = false)
  644. {
  645. $command = 'php ' . $this->get('filename');
  646. if ($options === false) {
  647. $command .= ' -d';
  648. if ($this->get('pid_file'))
  649. $command .= ' -p ' . $this->get('pid_file');
  650. if ($this->get('debug_workers'))
  651. $command .= ' --debugworkers';
  652. }
  653. else {
  654. $command .= ' ' . trim($options);
  655. }
  656. // We have to explicitly redirect output to /dev/null to prevent the exec() call from hanging
  657. $command .= ' > /dev/null';
  658. return $command;
  659. }
  660. /**
  661. * This will dump various runtime details to the log.
  662. * @example $ kill -10 [pid]
  663. * @return void
  664. */
  665. private function dump()
  666. {
  667. $workers = '';
  668. foreach($this->workers as $worker)
  669. $workers .= sprintf('%s %s [%s], ', $worker, $this->{$worker}->guid, $this->{$worker}->is_idle() ? 'AVAILABLE' : 'BUFFERING');
  670. $pretty_memory = function($bytes) {
  671. $kb = 1024; $mb = $kb * 1024; $gb = $mb * 1024;
  672. switch(true) {
  673. case $bytes > $gb: return sprintf('%sG', number_format($bytes / $gb, 2));
  674. case $bytes > $mb: return sprintf('%sM', number_format($bytes / $mb, 2));
  675. case $bytes > $kb: return sprintf('%sK', number_format($bytes / $kb, 2));
  676. default: return $bytes;
  677. }
  678. };
  679. $pretty_duration = function($seconds) {
  680. $m = 60; $h = $m * 60; $d = $h * 24;
  681. $out = '';
  682. switch(true) {
  683. case $seconds > $d:
  684. $out .= intval($seconds / $d) . 'd ';
  685. $seconds %= $d;
  686. case $seconds > $h:
  687. $out .= intval($seconds / $h) . 'h ';
  688. $seconds %= $h;
  689. case $seconds > $m:
  690. $out .= intval($seconds / $m) . 'm ';
  691. $seconds %= $m;
  692. default:
  693. $out .= "{$seconds}s";
  694. }
  695. return $out;
  696. };
  697. $pretty_bool = function($bool) {
  698. return ($bool ? 'Yes' : 'No');
  699. };
  700. $out = array();
  701. $out[] = "---------------------------------------------------------------------------------------------------";
  702. $out[] = "Application Runtime Statistics";
  703. $out[] = "---------------------------------------------------------------------------------------------------";
  704. $out[] = "Command: " . ($this->is('parent') ? $this->get('filename') : 'Forked Process from pid ' . $this->get('parent_pid'));
  705. $out[] = "Loop Interval: " . $this->loop_interval;
  706. $out[] = "Idle Probability " . $this->idle_probability;
  707. $out[] = "Restart Interval: " . $this->auto_restart_interval;
  708. $out[] = sprintf("Start Time: %s (%s)", $this->get('start_time'), date('Y-m-d H:i:s', $this->get('start_time')));
  709. $out[] = sprintf("Duration: %s (%s)", $this->runtime(), $pretty_duration($this->runtime()));
  710. $out[] = "Log File: " . $this->log_file();
  711. $out[] = "Daemon Mode: " . $pretty_bool($this->is('daemonized'));
  712. $out[] = "Shutdown Signal: " . $pretty_bool($this->is('shutdown'));
  713. $out[] = "Process Type: " . ($this->is('parent') ? 'Application Process' : 'Background Process');
  714. $out[] = "Plugins: " . implode(', ', $this->plugins);
  715. $out[] = "Workers: " . $workers;
  716. $out[] = sprintf("Memory: %s (%s)", memory_get_usage(true), $pretty_memory(memory_get_usage(true)));
  717. $out[] = sprintf("Peak Memory: %s (%s)", memory_get_peak_usage(true), $pretty_memory(memory_get_peak_usage(true)));
  718. $out[] = "Current User: " . get_current_user();
  719. $out[] = "Priority: " . pcntl_getpriority();
  720. $out[] = "Loop: duration, idle: " . implode(', ', $this->stats_mean()) . ' (Mean Seconds)';
  721. $out[] = "Stats sample size: " . count($this->stats);
  722. $this->log(implode("\n", $out));
  723. }
  724. /**
  725. * Time the execution loop and sleep an appropriate amount of time.
  726. * @param boolean $start
  727. * @return mixed
  728. */
  729. private function timer($start = false)
  730. {
  731. static $start_time = null;
  732. // Start the Stop Watch and Return
  733. if ($start)
  734. return $start_time = microtime(true);
  735. // End the Stop Watch
  736. // Determine if we should run the ON_IDLE tasks.
  737. // In timer based applications, determine if we have remaining time.
  738. // Otherwise apply the $idle_probability factor
  739. $end_time = $probability = null;
  740. if ($this->loop_interval) $end_time = ($start_time + $this->loop_interval() - 0.01);
  741. if ($this->idle_probability) $probability = (1 / $this->idle_probability);
  742. $is_idle = function() use($end_time, $probability) {
  743. if ($end_time)
  744. return microtime(true) < $end_time;
  745. if ($probability)
  746. return mt_rand(1, $probability) == 1;
  747. return false;
  748. };
  749. // If we have idle time, do any housekeeping tasks
  750. if ($is_idle()) {
  751. $this->dispatch(array(Core_Daemon::ON_IDLE), array($is_idle));
  752. }
  753. $stats = array();
  754. $stats['duration'] = microtime(true) - $start_time;
  755. $stats['idle'] = $this->loop_interval - $stats['duration'];
  756. // Suppress child signals during sleep to stop exiting forks/workers from interrupting the timer.
  757. // Note: SIGCONT (-18) signals are not suppressed and can be used to "wake up" the daemon.
  758. if ($stats['idle'] > 0) {
  759. pcntl_sigprocmask(SIG_BLOCK, array(SIGCHLD));
  760. usleep($stats['idle'] * 1000000);
  761. pcntl_sigprocmask(SIG_UNBLOCK, array(SIGCHLD));
  762. } else {
  763. // There is no time to sleep between intervals -- but we still need to give the CPU a break
  764. // Sleep for 1/100 a second.
  765. usleep(10000);
  766. if ($this->loop_interval > 0)
  767. $this->error('Run Loop Taking Too Long. Duration: ' . number_format($stats['duration'], 3) . ' Interval: ' . $this->loop_interval);
  768. }
  769. $this->stats[] = $stats;
  770. return $stats;
  771. }
  772. /**
  773. * If this is in daemon mode, provide an auto-restart feature.
  774. * This is designed to allow us to get a fresh stack, fresh memory allocation, etc.
  775. * @return boolean|void
  776. */
  777. private function auto_restart()
  778. {
  779. if (!$this->is('parent') || !$this->is('daemonized'))
  780. return;
  781. if ($this->runtime() < $this->auto_restart_interval || $this->auto_restart_interval < self::MIN_RESTART_SECONDS)
  782. return false;
  783. $this->restart();
  784. }
  785. /**
  786. * There are 2 paths to the daemon calling restart: The Auto Restart feature, and, also, if a fatal error
  787. * is encountered after it's been running for a while, it will attempt to re-start.
  788. * @return void;
  789. */
  790. public function restart()
  791. {
  792. if (!$this->is('parent') || !$this->is('daemonized'))
  793. return;
  794. $this->set('shutdown', true);
  795. $this->log('Restart Happening Now...');
  796. // We want to shutdown workers, release any lock files, and swap out the pid file (as applicable)
  797. // Basically put this into a walking-dead state by destructing everything while keeping this process alive
  798. // to actually orchestrate the restart.
  799. $this->__destruct();
  800. // Close the resource handles to prevent this process from hanging on the exec() output.
  801. if (is_resource(STDOUT)) fclose(STDOUT);
  802. if (is_resource(STDERR)) fclose(STDERR);
  803. if (is_resource(STDIN)) fclose(STDIN);
  804. // Close the static log handle to prevent it being inherrited by the new process.
  805. if (is_resource(self::$log_handle)) fclose(self::$log_handle);
  806. exec($this->command());
  807. // A new daemon process has been created. This one will stick around just long enough to clean up the worker processes.
  808. exit();
  809. }
  810. /**
  811. * Load any plugin that implements the Core_IPlugin.
  812. *
  813. * This is an object loader. What we care about is what object to create and where to put it. What we care about first
  814. * is an alias. This will be the name of the instance variable where the object will be set. If the alias also matches the name
  815. * of a class in Core/Plugin then it will just magically instantiate that class and be on its way. The following
  816. * two examples are identical:
  817. *
  818. * @example $this->plugin('ini');
  819. * @example $this->plugin('ini', new Core_Plugin_Ini() );
  820. *
  821. * In both of the preceding examples, a Core_Plugin_Ini object is available throughout your application object
  822. * as $this->ini.
  823. *
  824. * More complex (or just less magical) code can be used when appropriate. Want to load multiple instances of a plugin?
  825. * Want to use more meaningful names in your application instead of just duplicating part of the class name?
  826. * You can do all that too. This is simple dependency injection. Inject whatever object you want at runtime as long
  827. * as it implements Core_IPlugin.
  828. *
  829. * @example $this->plugin('credentials', new Core_Plugins_Ini());
  830. * $this->plugin('settings', new Core_Plugins_Ini());
  831. * $this->credentials->filename = '~/prod/credentials.ini';
  832. * $this->settings->filename = BASE_PATH . '/MyDaemon/settings.ini';
  833. * echo $this->credentials['mysql']['user']; // Echo the 'user' key in the 'mysql' section
  834. *
  835. * Note: As demonstrated, the alias is used simply as the name of a public instance variable on your application
  836. * object. All of the normal rules of reality apply: Aliases must be unique across plugins AND workers (which work
  837. * exactly like plugins in this respect). And both must be unique from any other instance or class vars used in
  838. * Core_Daemon or in your application superclass.
  839. *
  840. * Note: The Lock objects in Core/Lock are also Plugins and can be loaded in nearly the same way.
  841. * Take Core_Lock_File for instance. The only difference is that you cannot magically load it using the alias
  842. * 'file' alone. The Plugin loader would not know to look for the file in the Lock directory. In these instances
  843. * the prefix is necessary.
  844. * @example $this->plugin('Lock_File'); // Instantiated at $this->Lock_File
  845. *
  846. * @param string $alias
  847. * @param Core_IPlugin|null $instance
  848. * @return Core_IPlugin Returns an instance of the plugin
  849. * @throws Exception
  850. */
  851. protected function plugin($alias, Core_IPlugin $instance = null)
  852. {
  853. $this->check_alias($alias);
  854. if ($instance === null) {
  855. // This if wouldn't be necessary if /Lock lived inside /Plugin.
  856. // Now that Locks are plugins in every other way, maybe it should be moved. OTOH, do we really need 4
  857. // levels of directory depth in a project with like 10 files...?
  858. if (substr(strtolower($alias), 0, 5) == 'lock_')
  859. $class = 'Core_' . ucfirst($alias);
  860. else
  861. $class = 'Core_Plugin_' . ucfirst($alias);
  862. if (class_exists($class, true)) {
  863. $interfaces = class_implements($class, true);
  864. if (is_array($interfaces) && isset($interfaces['Core_IPlugin'])) {
  865. $instance = new $class($this->getInstance());
  866. }
  867. }
  868. }
  869. if (!is_object($instance)) {
  870. throw new Exception(__METHOD__ . " Failed. Could Not Load Plugin '{$alias}'");
  871. }
  872. $this->{$alias} = $instance;
  873. $this->plugins[] = $alias;
  874. return $instance;
  875. }
  876. /**
  877. * Create a persistent Worker process. This is an object loader similar to Core_Daemon::plugin().
  878. *
  879. * @param String $alias The name of the worker -- Will be instantiated at $this->{$alias}
  880. * @param callable|Core_IWorker $worker An object of type Core_Worker OR a callable (function, callback, closure)
  881. * @param Core_IWorkerVia $via A Core_IWorkerVia object that defines the medium for IPC (In theory could be any message queue, redis, memcache, etc)
  882. * @return Core_Worker_ObjectMediator Returns a Core_Worker class that can be used to interact with the Worker
  883. * @todo Use 'callable' type hinting if/when we move to a php 5.4 requirement.
  884. */
  885. protected function worker($alias, $worker, Core_IWorkerVia $via = null)
  886. {
  887. if (!$this->is('parent'))
  888. // While in theory there is nothing preventing you from creating workers in child processes, supporting it
  889. // would require changing a lot of error handling and process management code and I don't really see the value in it.
  890. throw new Exception(__METHOD__ . ' Failed. You cannot create workers in a background processes.');
  891. if ($via === null)
  892. $via = new Core_Worker_Via_SysV();
  893. $this->check_alias($alias);
  894. switch (true) {
  895. case is_object($worker) && !is_a($worker, 'Closure'):
  896. $mediator = new Core_Worker_ObjectMediator($alias, $this, $via);
  897. // Ensure that there are no reserved method names in the worker object -- Determine if there will
  898. // be a collision between worker methods and public methods on the Mediator class
  899. // Exclude any methods required by the Core_IWorker interface from the check.
  900. $intersection = array_intersect(get_class_methods($worker), get_class_methods($mediator));
  901. $intersection = array_diff($intersection, get_class_methods('Core_IWorker'));
  902. if (!empty($intersection))
  903. throw new Exception(sprintf('%s Failed. Your worker class "%s" contains restricted method names: %s.',
  904. __METHOD__, get_class($worker), implode(', ', $intersection)));
  905. $mediator->setObject($worker);
  906. break;
  907. case is_callable($worker):
  908. $mediator = new Core_Worker_FunctionMediator($alias, $this, $via);
  909. $mediator->setFunction($worker);
  910. break;
  911. default:
  912. throw new Exception(__METHOD__ . ' Failed. Could Not Load Worker: ' . $alias);
  913. }
  914. $this->workers[] = $alias;
  915. $this->{$alias} = $mediator;
  916. return $this->{$alias};
  917. }
  918. /**
  919. * Simple function to validate that alises for Plugins or Workers won't interfere with each other or with existing daemon properties.
  920. * @param $alias
  921. * @throws Exception
  922. */
  923. private function check_alias($alias) {
  924. if (empty($alias) || !is_scalar($alias))
  925. throw new Exception("Invalid Alias. Identifiers must be scalar.");
  926. if (isset($this->{$alias}))
  927. throw new Exception("Invalid Alias. The identifier `{$alias}` is already in use or is reserved");
  928. }
  929. /**
  930. * Handle command line arguments. To easily extend, just add parent::getopt at the TOP of your overloading method.
  931. * @return void
  932. */
  933. protected function getopt()
  934. {
  935. $opts = getopt('hHiI:o:dp:', array('install', 'recoverworkers', 'debugworkers', 'verbose'));
  936. if (isset($opts['H']) || isset($opts['h']))
  937. $this->show_help();
  938. if (isset($opts['i']))
  939. $this->show_install_instructions();
  940. if (isset($opts['I']))
  941. $this->create_init_script($opts['I'], isset($opts['install']));
  942. if (isset($opts['d'])) {
  943. if (pcntl_fork() > 0)
  944. exit();
  945. $this->pid(getmypid()); // We have a new pid now
  946. }
  947. $this->set('daemonized', isset($opts['d']));
  948. $this->set('recover_workers', isset($opts['recoverworkers']));
  949. $this->set('debug_workers', isset($opts['debugworkers']));
  950. $this->set('stdout', !$this->is('daemonized') && !$this->get('debug_workers'));
  951. $this->set('verbose', $this->is('stdout') && isset($opts['verbose']));
  952. if (isset($opts['p'])) {
  953. $handle = @fopen($opts['p'], 'w');
  954. if (!$handle)
  955. $this->show_help('Unable to write PID to ' . $opts['p']);
  956. fwrite($handle, $this->pid);
  957. fclose($handle);
  958. $this->set('pid_file', $opts['p']);
  959. }
  960. }
  961. /**
  962. * Print a Help Block and Exit. Optionally Print $msg along with it.
  963. * @param string $msg
  964. * @return void
  965. */
  966. protected function show_help($msg = '')
  967. {
  968. $out = array('');
  969. if ($msg) {
  970. $out[] = '';
  971. $out[] = 'ERROR:';
  972. $out[] = ' ' . wordwrap($msg, 72, "\n ");
  973. }
  974. echo get_class($this);
  975. $out[] = 'USAGE:';
  976. $out[] = ' $ ' . basename($this->get('filename')) . ' -H | -i | -I TEMPLATE_NAME [--install] | [-d] [-p PID_FILE] [--verbose] [--debugworkers]';
  977. $out[] = '';
  978. $out[] = 'OPTIONS:';
  979. $out[] = ' -H Shows this help';
  980. $out[] = ' -i Print any daemon install instructions to the screen';
  981. $out[] = ' -I Create init/config script';
  982. $out[] = ' You must pass in a name of a template from the /Templates directory';
  983. $out[] = ' OPTIONS:';
  984. $out[] = ' --install';
  985. $out[] = ' Install the script to /etc/init.d. Otherwise just output the script to stdout.';
  986. $out[] = '';
  987. $out[] = ' -d Daemon, detach and run in the background';
  988. $out[] = ' -p PID_FILE File to write process ID out to';
  989. $out[] = '';
  990. $out[] = ' --verbose';
  991. $out[] = ' Include debug messages in the application log.';
  992. $out[] = ' Note: When run as a daemon (-d) or with a debug shell (--debugworkers), application log messages are written only to the log file.';
  993. $out[] = '';
  994. $out[] = ' --debugworkers';
  995. $out[] = ' Run workers under a debug console. Provides tools to debug the inter-process communication between workers.';
  996. $out[] = ' Console will only be displayed if Workers are used in your daemon';
  997. $out[] = '';
  998. $out[] = '';
  999. echo implode("\n", $out);
  1000. exit();
  1001. }
  1002. /**
  1003. * Print any install instructions and Exit.
  1004. * Could be anything from copying init.d scripts, setting crontab entries, creating executable or writable directories, etc.
  1005. * Add instructions from your daemon by adding them one by one: $this->install_instructions[] = 'Do foo'
  1006. * @return void
  1007. */
  1008. protected function show_install_instructions()
  1009. {
  1010. echo get_class($this) . " Installation Instructions:\n\n - ";
  1011. echo implode("\n - ", $this->install_instructions);
  1012. echo "\n";
  1013. exit();
  1014. }
  1015. /**
  1016. * Create and output an init script for this daemon to provide start/stop/restart functionality.
  1017. * Uses templates in the /Templates directory to produce scripts for different process managers and linux distros.
  1018. * When you create an init script, you should chmod it to 0755.
  1019. *
  1020. * @param string $template The name of a template from the /Templates directory
  1021. * @param bool $install When true, the script will be created in the init.d directory and final setup instructions will be printed to stdout
  1022. * @return void
  1023. */
  1024. protected function create_init_script($template_name, $install = false)
  1025. {
  1026. $template = dirname(__FILE__) . '/Templates/' . $template_name;
  1027. if (!file_exists($template))
  1028. $this->show_help("Invalid Template Name '{$template_name}'");
  1029. $daemon = get_class($this);
  1030. $script = sprintf(
  1031. file_get_contents($template),
  1032. $daemon,
  1033. $this->command("-d -p /var/run/{$daemon}.pid")
  1034. );
  1035. if (!$install) {
  1036. echo $script;
  1037. echo "\n\n";
  1038. exit;
  1039. }
  1040. // Print out template-specific setup instructions
  1041. switch($template_name) {
  1042. case 'init_ubuntu':
  1043. $filename = '/etc/init.d/' . $daemon;
  1044. $instructions = "\n - To run on startup on RedHat/CentOS: sudo chkconfig --add {$filename}";
  1045. break;
  1046. default:
  1047. $instructions = '';
  1048. }
  1049. @file_put_contents($filename, $script);
  1050. @chmod($filename, 0755);
  1051. if (file_exists($filename) == false || is_executable($filename) == false)
  1052. $this->show_help("* Must Be Run as Sudo\n * Could Not Write Config File");
  1053. echo "Init Scripts Created Successfully!";
  1054. echo $instructions;
  1055. echo "\n\n";
  1056. exit();
  1057. }
  1058. /**
  1059. * Return the running time in Seconds
  1060. * @return integer
  1061. */
  1062. public function runtime()
  1063. {
  1064. return time() - $this->get('start_time');
  1065. }
  1066. /**
  1067. * Return a list containing the mean duration and idle time of the daemons event loop, ignoring the longest and shortest 5%
  1068. * Note: Stats data is trimmed periodically and is not likely to have more than 200 rows.
  1069. * @param int $last Limit the working set to the last n iteration
  1070. * @return Array A list as array(duration, idle) averages.
  1071. */
  1072. public function stats_mean($last = 100)
  1073. {
  1074. if (count($this->stats) < $last) {
  1075. $data = $this->stats;
  1076. } else {
  1077. $data = array_slice($this->stats, -$last);
  1078. }
  1079. $count = count($data);
  1080. $n = ceil($count * 0.05);
  1081. // Sort the $data by duration and remove the top and bottom $n rows
  1082. $duration = array();
  1083. for($i=0; $i<$count; $i++) {
  1084. $duration[$i] = $data[$i]['duration'];
  1085. }
  1086. array_multisort(

Large files files are truncated, but you can click here to view the full file