service.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  1. <?php
  2. /*
  3. * FusionPBX
  4. * Version: MPL 1.1
  5. *
  6. * The contents of this file are subject to the Mozilla Public License Version
  7. * 1.1 (the "License"); you may not use this file except in compliance with
  8. * the License. You may obtain a copy of the License at
  9. * http://www.mozilla.org/MPL/
  10. *
  11. * Software distributed under the License is distributed on an "AS IS" basis,
  12. * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  13. * for the specific language governing rights and limitations under the
  14. * License.
  15. *
  16. * The Original Code is FusionPBX
  17. *
  18. * The Initial Developer of the Original Code is
  19. * Mark J Crane <[email protected]>
  20. * Portions created by the Initial Developer are Copyright (C) 2008-2024
  21. * the Initial Developer. All Rights Reserved.
  22. *
  23. * Contributor(s):
  24. * Mark J Crane <[email protected]>
  25. * Tim Fry <[email protected]>
  26. */
  27. /**
  28. * Service class
  29. * @version 1.00
  30. * @author Tim Fry <[email protected]>
  31. */
  32. abstract class service {
  33. const VERSION = "1.00";
  34. /**
  35. * Track the internal loop. It is recommended to use this variable to control the loop inside the run function. See the example
  36. * below the class for a more complete explanation
  37. * @var bool
  38. */
  39. protected $running;
  40. /**
  41. * current debugging level for output to syslog
  42. * @var int Syslog level
  43. */
  44. protected static $log_level = LOG_INFO;
  45. /**
  46. * config object
  47. * @var config config object
  48. */
  49. protected static $config;
  50. /**
  51. * Holds the parsed options from the command line
  52. * @var array
  53. */
  54. protected static $parsed_command_options;
  55. /**
  56. * Operating System process identification file
  57. * @var string
  58. */
  59. private static $pid_file = "";
  60. /**
  61. * Cli Options Array
  62. * @var array
  63. */
  64. protected static $available_command_options = [];
  65. /**
  66. * Holds the configuration file location
  67. * @var string
  68. */
  69. protected static $config_file = "";
  70. /**
  71. * Fork the service to it's own process ID
  72. * @var bool
  73. */
  74. protected static $forking_enabled = true;
  75. /**
  76. * Child classes must provide a mechanism to reload settings
  77. */
  78. abstract protected function reload_settings(): void;
  79. /**
  80. * Method to start the child class internal loop
  81. */
  82. abstract public function run(): int;
  83. /**
  84. * Display version notice
  85. */
  86. abstract protected static function display_version(): void;
  87. /**
  88. * Called when the display_help_message is run in the base class for extra command line parameter explanation
  89. */
  90. abstract protected static function set_command_options();
  91. /**
  92. * Open a log when created.
  93. * <p>NOTE:<br>
  94. * This is a protected function so it can not be called using the keyword 'new' outside of this class or a child
  95. * class. This is due to the requirement to set signal handlers for the POSIX system outside of the constructor.
  96. * PHP seems to have an issue on some versions where setting a signal handler while in the constructor (even
  97. * calling another method from the constructor) will fail to register the signal handlers.</p>
  98. */
  99. protected function __construct() {
  100. openlog('[php][' . self::class . ']', LOG_CONS | LOG_NDELAY | LOG_PID, LOG_DAEMON);
  101. }
  102. public function __destruct() {
  103. //ensure we unlink the correct PID file if needed
  104. if (self::is_running()) {
  105. unlink(self::$pid_file);
  106. self::log("Initiating Shutdown...", LOG_NOTICE);
  107. $this->running = false;
  108. }
  109. //this should remain the last statement to execute before exit
  110. closelog();
  111. }
  112. /**
  113. * Shutdown process gracefully
  114. */
  115. public static function shutdown() {
  116. exit();
  117. }
  118. public static function send_shutdown() {
  119. if (self::is_any_running()) {
  120. self::send_signal(SIGTERM);
  121. } else {
  122. die("Service Not Started\n");
  123. }
  124. }
  125. // register signal handlers
  126. private function register_signal_handlers() {
  127. // Allow the calls to be made while the main loop is running
  128. pcntl_async_signals(true);
  129. // A signal listener to reload the service for any config changes in the database
  130. pcntl_signal(SIGUSR1, [$this, 'reload_settings']);
  131. pcntl_signal(SIGHUP, [$this, 'reload_settings']);
  132. // A signal listener to stop the service
  133. pcntl_signal(SIGUSR2, [self::class, 'shutdown']);
  134. pcntl_signal(SIGTERM, [self::class, 'shutdown']);
  135. }
  136. /**
  137. * Extracts the short options from the cli options array and returns a string. The resulting string must
  138. * return a single string with all options in the string such as 'rxc:'.
  139. * This can be overridden by the child class.
  140. * @return string
  141. */
  142. protected static function get_short_options(): string {
  143. return implode('' , array_map(function ($option) { return $option['short_option']; }, self::$available_command_options));
  144. }
  145. /**
  146. * Extracts the long options from the cli options array and returns an array. The resulting array must
  147. * return a single dimension array with an integer indexed key but does not have to be sequential order.
  148. * This can be overridden by the child class.
  149. * @return array
  150. */
  151. protected static function get_long_options(): array {
  152. return array_map(function ($option) { return $option['long_option']; }, self::$available_command_options);
  153. }
  154. /**
  155. * Method that will retrieve the callbacks from the cli options array
  156. * @param string $set_option
  157. * @return array
  158. */
  159. protected static function get_user_callbacks_from_available_options(string $set_option): array {
  160. //match the available option to the set option and return the callback function that needs to be called
  161. foreach(self::$available_command_options as $option) {
  162. $short_option = $option['short_option'] ?? '';
  163. if (str_ends_with($short_option, ':')) {
  164. $short_option = rtrim($short_option, ':');
  165. }
  166. $long_option = $option['long_option'] ?? '';
  167. if (str_ends_with($long_option, ':')) {
  168. $long_option = rtrim($long_option, ':');
  169. }
  170. if ($short_option === $set_option ||
  171. $long_option === $set_option) {
  172. return $option['functions'] ?? [$option['function']] ?? [];
  173. }
  174. }
  175. return [];
  176. }
  177. /**
  178. * Parse CLI options using getopt()
  179. * @return void
  180. */
  181. protected static function parse_service_command_options(): void {
  182. //ensure we have a PID so that reload and exit send commands work
  183. if (empty(self::$pid_file)) {
  184. self::$pid_file = self::get_pid_filename();
  185. }
  186. //base class short options
  187. self::$available_command_options = self::base_command_options();
  188. //get the options from the child class
  189. static::set_command_options();
  190. //collapse short options to a string
  191. $short_options = self::get_short_options();
  192. //isolate long options
  193. $long_options = self::get_long_options();
  194. //parse the short and long options
  195. $options = getopt($short_options, $long_options);
  196. //make the options available to the child object
  197. if ($options !== false) {
  198. self::$parsed_command_options = $options;
  199. } else {
  200. //make sure the command_options are reset
  201. self::$parsed_command_options = [];
  202. //if the options are empty there is nothing left to do
  203. return;
  204. }
  205. //notify user
  206. self::log("CLI Options detected: " . implode(",", self::$parsed_command_options), LOG_DEBUG);
  207. //loop through the parsed options given on the command line
  208. foreach ($options as $option_key => $option_value) {
  209. //get the function responsible for handling the cli option
  210. $funcs = self::get_user_callbacks_from_available_options($option_key);
  211. //ensure it was found before we take action
  212. if (!empty($funcs)) {
  213. //check for more than one function to be called is permitted
  214. if (is_array($funcs)) {
  215. //call each one
  216. foreach($funcs as $func) {
  217. //use the best method to call the function
  218. self::call_function($func, $option_value);
  219. }
  220. } else {
  221. //single function call
  222. self::call_function($func, $option_value);
  223. }
  224. }
  225. }
  226. }
  227. //
  228. // Calls a function using the best suited PHP method
  229. //
  230. private static function call_function($function, $args) {
  231. if ($function === 'exit') {
  232. //check for exit
  233. exit($args);
  234. } elseif ($function instanceof Closure || function_exists($function)) {
  235. //globally available function or closure
  236. $function($args);
  237. } else {
  238. static::$function($args);
  239. }
  240. }
  241. /**
  242. * Checks the file system for a pid file that matches the process ID from this running instance
  243. * @return bool true if pid exists and false if not
  244. */
  245. public static function is_running(): bool {
  246. return posix_getpid() === self::get_service_pid();
  247. }
  248. public static function is_any_running(): bool {
  249. return self::get_service_pid() !== false;
  250. }
  251. /**
  252. * Returns the operating system service PID or false if it is not yet running
  253. * @return bool|int PID or false if not running
  254. */
  255. protected static function get_service_pid() {
  256. if (file_exists(self::$pid_file)) {
  257. $pid = file_get_contents(self::$pid_file);
  258. if (function_exists('posix_getsid')) {
  259. if (posix_getsid($pid) !== false) {
  260. //return the pid for reloading configuration
  261. return $pid;
  262. }
  263. } else {
  264. if (file_exists('/proc/' . $pid)) {
  265. //return the pid for reloading configuration
  266. return $pid;
  267. }
  268. }
  269. }
  270. return false;
  271. }
  272. /**
  273. * Create an operating system PID file removing any existing PID file
  274. */
  275. private function create_service_pid() {
  276. // Set the pid filename
  277. $basename = basename(self::$pid_file, '.pid');
  278. $pid = getmypid();
  279. // Remove the old pid file
  280. if (file_exists(self::$pid_file)) {
  281. unlink(self::$pid_file);
  282. }
  283. // Show the details to the user
  284. self::log("Service : $basename", LOG_INFO);
  285. self::log("Process ID: $pid", LOG_INFO);
  286. self::log("PID File : " . self::$pid_file, LOG_INFO);
  287. // Save the pid file
  288. file_put_contents(self::$pid_file, $pid);
  289. }
  290. /**
  291. * Creates the service directory to store the PID
  292. * @throws Exception thrown when the service directory is unable to be created
  293. */
  294. private function create_service_directory() {
  295. //make sure the /var/run/fusionpbx directory exists
  296. if (!file_exists('/var/run/fusionpbx')) {
  297. $result = mkdir('/var/run/fusionpbx', 0777, true);
  298. if (!$result) {
  299. throw new Exception('Failed to create /var/run/fusionpbx');
  300. }
  301. }
  302. }
  303. /**
  304. * Parses the debug level to an integer and stores it in the class for syslog use
  305. * @param string $debug_level Debug level with any of the Linux system log levels
  306. */
  307. protected static function set_debug_level(string $debug_level) {
  308. // Map user input log level to syslog constant
  309. switch ($debug_level) {
  310. case '0':
  311. case 'emergency':
  312. self::$log_level = LOG_EMERG; // Hardware failures
  313. break;
  314. case '1':
  315. case 'alert':
  316. self::$log_level = LOG_ALERT; // Loss of network connection or a condition that should be corrected immediately
  317. break;
  318. case '2':
  319. case 'critical':
  320. self::$log_level = LOG_CRIT; // Condition like low disk space
  321. break;
  322. case '3':
  323. case 'error':
  324. self::$log_level = LOG_ERR; // Database query failure, file not found
  325. break;
  326. case '4':
  327. case 'warning':
  328. self::$log_level = LOG_WARNING; // Deprecated function usage, approaching resource limits
  329. break;
  330. case '5':
  331. case 'notice':
  332. self::$log_level = LOG_NOTICE; // Normal conditions
  333. break;
  334. case '6':
  335. case 'info':
  336. self::$log_level = LOG_INFO; // Informational
  337. break;
  338. case '7':
  339. case 'debug':
  340. self::$log_level = LOG_DEBUG; // Debugging
  341. break;
  342. default:
  343. self::$log_level = LOG_NOTICE; // Default to NOTICE if invalid level
  344. }
  345. }
  346. /**
  347. * Show memory usage to the user
  348. */
  349. protected static function show_mem_usage() {
  350. //current memory
  351. $memory_usage = memory_get_usage();
  352. //peak memory
  353. $memory_peak = memory_get_peak_usage();
  354. self::log('Current memory: ' . round($memory_usage / 1024) . " KB", LOG_INFO);
  355. self::log('Peak memory: ' . round($memory_peak / 1024) . " KB", LOG_INFO);
  356. }
  357. /**
  358. * Logs to the system log
  359. * @param string $message
  360. * @param int $level
  361. */
  362. protected static function log(string $message, int $level = null) {
  363. // Use default log level if not provided
  364. if ($level === null) {
  365. $level = self::$log_level;
  366. }
  367. //enable sending message to the console directly
  368. if (self::$log_level === LOG_DEBUG || !self::$forking_enabled) {
  369. echo $message . "\n";
  370. }
  371. // Log the message to syslog
  372. syslog($level, 'fusionpbx[' . posix_getpid() . ']: ['.static::class.'] '.$message);
  373. }
  374. /**
  375. * Returns a file safe class name with \ from namespaces converted to _
  376. * @return string file safe name
  377. */
  378. protected static function base_file_name(): string {
  379. return str_replace('\\', "_", static::class);
  380. }
  381. /**
  382. * Returns only the name of the class without namespace
  383. * @return string base class name
  384. */
  385. protected static function base_class_name(): string {
  386. $class_and_namespace = explode('\\', static::class);
  387. return array_pop($class_and_namespace);
  388. }
  389. /**
  390. * Write a standard copyright notice to the console
  391. * @return void
  392. */
  393. public static function display_copyright(): void {
  394. echo "FusionPBX\n";
  395. echo "Version: MPL 1.1\n";
  396. echo "\n";
  397. echo "The contents of this file are subject to the Mozilla Public License Version\n";
  398. echo "1.1 (the \"License\"); you may not use this file except in compliance with\n";
  399. echo "the License. You may obtain a copy of the License at\n";
  400. echo "http://www.mozilla.org/MPL/\n";
  401. echo "\n";
  402. echo "Software distributed under the License is distributed on an \"AS IS\" basis,\n";
  403. echo "WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License\n";
  404. echo "for the specific language governing rights and limitations under the\n";
  405. echo "License.\n";
  406. echo "\n";
  407. echo "The Original Code is FusionPBX\n";
  408. echo "\n";
  409. echo "The Initial Developer of the Original Code is\n";
  410. echo "Mark J Crane <[email protected]>\n";
  411. echo "Portions created by the Initial Developer are Copyright (C) 2008-2023\n";
  412. echo "the Initial Developer. All Rights Reserved.\n";
  413. echo "\n";
  414. echo "Contributor(s):\n";
  415. echo "Mark J Crane <[email protected]>\n";
  416. echo "Tim Fry <[email protected]>\n";
  417. echo "\n";
  418. }
  419. /**
  420. * Sends the shutdown signal to the service using a posix signal.
  421. * <p>NOTE:<br>
  422. * The signal will not be received from the service if the
  423. * command is sent from a user that has less privileges then
  424. * the running service. For example, if the service is started
  425. * by user root and then the command line option '-r' is given
  426. * as user www-data, the service will not receive this signal
  427. * because the OS will not allow the signal to be passed to a
  428. * more privileged user due to security concerns. This would
  429. * be the main reason why you must run a 'systemctl' or a
  430. * 'service' command as root user. It is possible to start the
  431. * service with user www-data and then the web UI would in fact
  432. * be able to send the reload signal to the running service.</p>
  433. */
  434. public static function send_signal($posix_signal) {
  435. $signal_name = "";
  436. switch ($posix_signal) {
  437. case SIGHUP:
  438. case SIGUSR1:
  439. $signal_name = "Reload";
  440. break;
  441. case SIGTERM:
  442. case SIGUSR2:
  443. $signal_name = "Shutdown";
  444. break;
  445. }
  446. $pid = self::get_service_pid();
  447. if ($pid === false) {
  448. self::log("service not running", LOG_EMERG);
  449. } else {
  450. if (posix_kill((int) $pid, $posix_signal) ) {
  451. echo "Sent $signal_name\n";
  452. } else {
  453. $err = posix_strerror(posix_get_last_error());
  454. echo "Failed to send $signal_name: $err\n";
  455. }
  456. }
  457. }
  458. /**
  459. * Display a basic help message to the user for using service
  460. */
  461. protected static function display_help_message(): void {
  462. //get the classname of the child class
  463. $class_name = self::base_class_name();
  464. //get the widest options for proper alignment
  465. $width_short = max(array_map(function ($arr) { return strlen($arr['short_description'] ?? ''); }, self::$available_command_options));
  466. $width_long = max(array_map(function ($arr) { return strlen($arr['long_description' ] ?? ''); }, self::$available_command_options));
  467. //display usage help using the class name of child
  468. echo "Usage: php $class_name [options]\n";
  469. //display the options aligned to the widest short and long options
  470. echo "Options:\n";
  471. foreach (self::$available_command_options as $option) {
  472. printf("%-{$width_short}s %-{$width_long}s %s\n",
  473. $option['short_description'],
  474. $option['long_description'],
  475. $option['description']
  476. );
  477. }
  478. }
  479. public static function send_reload() {
  480. if (self::is_any_running()) {
  481. self::send_signal(SIGUSR1);
  482. } else {
  483. die("Service Not Started\n");
  484. }
  485. exit();
  486. }
  487. //
  488. // Options built-in to the base service class. These can be overridden with the child class
  489. // or they can be extended using the array
  490. //
  491. private static function base_command_options(): array {
  492. //put the display for help in an array so we can calculate width
  493. $help_options = [];
  494. $index = 0;
  495. $help_options[$index]['short_option'] = 'v';
  496. $help_options[$index]['long_option'] = 'version';
  497. $help_options[$index]['description'] = 'Show the version information';
  498. $help_options[$index]['short_description'] = '-v';
  499. $help_options[$index]['long_description'] = '--version';
  500. $help_options[$index]['functions'][] = 'display_version';
  501. $help_options[$index]['functions'][] = 'shutdown';
  502. $index++;
  503. $help_options[$index]['short_option'] = 'h';
  504. $help_options[$index]['long_option'] = 'help';
  505. $help_options[$index]['description'] = 'Show the version and help message';
  506. $help_options[$index]['short_description'] = '-h';
  507. $help_options[$index]['long_description'] = '--help';
  508. $help_options[$index]['functions'][] = 'display_version';
  509. $help_options[$index]['functions'][] = 'display_help_message';
  510. $help_options[$index]['functions'][] = 'shutdown';
  511. $index++;
  512. $help_options[$index]['short_option'] = 'a';
  513. $help_options[$index]['long_option'] = 'about';
  514. $help_options[$index]['description'] = 'Show the version and copyright information';
  515. $help_options[$index]['short_description'] = '-a';
  516. $help_options[$index]['long_description'] = '--about';
  517. $help_options[$index]['functions'][] = 'display_version';
  518. $help_options[$index]['functions'][] = 'display_copyright';
  519. $help_options[$index]['functions'][] = 'shutdown';
  520. $index++;
  521. $help_options[$index]['short_option'] = 'r';
  522. $help_options[$index]['long_option'] = 'reload';
  523. $help_options[$index]['description'] = 'Reload settings for an already running service';
  524. $help_options[$index]['short_description'] = '-r';
  525. $help_options[$index]['long_description'] = '--reload';
  526. $help_options[$index]['functions'][] = 'send_reload';
  527. $index++;
  528. $help_options[$index]['short_option'] = 'd:';
  529. $help_options[$index]['long_option'] = 'debug:';
  530. $help_options[$index]['description'] = 'Set the syslog level between 0 (EMERG) and 7 (DEBUG). 5 (INFO) is default';
  531. $help_options[$index]['short_description'] = '-d <level>';
  532. $help_options[$index]['long_description'] = '--debug <level>';
  533. $help_options[$index]['functions'][] = 'set_debug_level';
  534. $index++;
  535. $help_options[$index]['short_option'] = 'c:';
  536. $help_options[$index]['long_option'] = 'config:';
  537. $help_options[$index]['description'] = 'Full path and file name of the configuration file to use. /etc/fusionpbx/config.conf or /usr/local/etc/fusionpbx/config.conf on FreeBSD is default';
  538. $help_options[$index]['short_description'] = '-c <path>';
  539. $help_options[$index]['long_description'] = '--config <path>';
  540. $help_options[$index]['functions'][] = 'set_config_file';
  541. $index++;
  542. $help_options[$index]['short_option'] = '1';
  543. $help_options[$index]['long_option'] = 'no-fork';
  544. $help_options[$index]['description'] = 'Do not fork the process';
  545. $help_options[$index]['short_description'] = '-1';
  546. $help_options[$index]['long_description'] = '--no-fork';
  547. $help_options[$index]['functions'][] = 'set_no_fork';
  548. $index++;
  549. $help_options[$index]['short_option'] = 'x';
  550. $help_options[$index]['long_option'] = 'exit';
  551. $help_options[$index]['description'] = 'Exit the service gracefully';
  552. $help_options[$index]['short_description'] = '-x';
  553. $help_options[$index]['long_description'] = '--exit';
  554. $help_options[$index]['functions'][] = 'send_shutdown';
  555. $help_options[$index]['functions'][] = 'shutdown';
  556. return $help_options;
  557. }
  558. /**
  559. * Set to not fork when started
  560. */
  561. public static function set_no_fork() {
  562. echo "Running in forground\n";
  563. self::$forking_enabled = false;
  564. }
  565. /**
  566. * Set the configuration file location to use for a config object
  567. */
  568. public static function set_config_file(string $file = '/etc/fusionpbx/config.conf') {
  569. if (empty(self::$config_file)) {
  570. self::$config_file = $file;
  571. }
  572. self::$config = new config(self::$config_file);
  573. }
  574. /**
  575. * Appends the CLI option to the list given to the user as a command line argument.
  576. * @param command_option $option
  577. * @return int The index of the item added
  578. */
  579. public static function append_command_option(command_option $option): int {
  580. $index = count(self::$available_command_options);
  581. self::$available_command_options[$index] = $option->to_array();
  582. return $index;
  583. }
  584. /**
  585. * Adds an option to the command line parameters
  586. * @param string $short_option
  587. * @param string $long_option
  588. * @param string $description
  589. * @param string $short_description
  590. * @param string $long_description
  591. * @param string $callback
  592. * @return int The index of the item added
  593. */
  594. public static function add_command_option(string $short_option, string $long_option, string $description, string $short_description = '', string $long_description = '', ...$callback): int {
  595. //use the option as the description if not filled in
  596. if (empty($short_description)) {
  597. $short_description = '-' . $short_option;
  598. if (str_ends_with($short_option, ':')) {
  599. $short_description .= " <setting>";
  600. }
  601. }
  602. if (empty($long_description)) {
  603. $long_description = '-' . $long_option;
  604. if (str_ends_with($long_option, ':')) {
  605. $long_description .= " <setting>";
  606. }
  607. }
  608. $index = count(self::$available_command_options);
  609. self::$available_command_options[$index]['short_option'] = $short_option;
  610. self::$available_command_options[$index]['long_option'] = $long_option;
  611. self::$available_command_options[$index]['description'] = $description;
  612. self::$available_command_options[$index]['short_description'] = $short_description;
  613. self::$available_command_options[$index]['long_description'] = $long_description;
  614. self::$available_command_options[$index]['functions'] = $callback;
  615. return $index;
  616. }
  617. /**
  618. * Returns the process ID filename used for a service
  619. * @return string file name used for the process identifier
  620. */
  621. public static function get_pid_filename(): string {
  622. return '/var/run/fusionpbx/' . self::base_file_name() . '.pid';
  623. }
  624. /**
  625. * Sets the following:
  626. * - execution time to unlimited
  627. * - location for PID file
  628. * - parses CLI options
  629. * - ensures folder structure exists
  630. * - registers signal handlers
  631. */
  632. private function init() {
  633. // Increase limits
  634. set_time_limit(0);
  635. ini_set('max_execution_time', 0);
  636. ini_set('memory_limit', '512M');
  637. //set the PID file
  638. self::$pid_file = self::get_pid_filename();
  639. //register the shutdown function
  640. register_shutdown_function([$this, 'shutdown']);
  641. // Ensure we have only one instance
  642. if (self::is_any_running()) {
  643. self::log("Service already running", LOG_ERR);
  644. exit();
  645. }
  646. // Ensure directory creation for pid location
  647. $this->create_service_directory();
  648. // Create a process identifier file
  649. $this->create_service_pid();
  650. // Set the signal handlers for reloading
  651. $this->register_signal_handlers();
  652. // We are now considered running
  653. $this->running = true;
  654. }
  655. /**
  656. * Creates a system service that will run in the background
  657. * @return self
  658. */
  659. public static function create(): self {
  660. //can only start from command line
  661. defined('STDIN') or die('Unauthorized');
  662. //parse the cli options and store them statically
  663. self::parse_service_command_options();
  664. //fork process
  665. if (self::$forking_enabled) {
  666. echo "Running in daemon mode\n";
  667. //force launching in a seperate process
  668. if ($pid = pcntl_fork()) {
  669. exit;
  670. }
  671. if ($cid = pcntl_fork()) {
  672. exit;
  673. }
  674. }
  675. //TODO remove updated settings object after merge
  676. if (file_exists( __DIR__ . '/settings.php')) {
  677. require_once __DIR__ . '/settings.php';
  678. }
  679. //TODO remove global functions after merge
  680. if (file_exists(dirname(__DIR__).'/functions.php')) {
  681. require_once dirname(__DIR__).'/functions.php';
  682. }
  683. //create the config object if not already created
  684. if (self::$config === null) {
  685. self::$config = new config(self::$config_file);
  686. }
  687. //get the name of child object
  688. $class = self::base_class_name();
  689. //create the child object
  690. $service = new $class();
  691. //initialize the service
  692. $service->init();
  693. //return the initialized object
  694. return $service;
  695. }
  696. }
  697. /*
  698. * Example
  699. *
  700. * The child_service class must be used to demonstrate the base_service because base_service is abstract. This means that you
  701. * cannot use the syntax of:
  702. * $service = new service(); //throws fatal error
  703. * $service->run(); //never reaches this statement
  704. *
  705. * Instead, you must use a class that will extend the service class like this:
  706. * $service = child_service::create();
  707. * $service->run();
  708. * (make the code below more readable by putting)
  709. * ( in the '/' line below to complete the comment section )
  710. *
  711. //
  712. // A class that extends base_service must implement 4 functions:
  713. // - run() This is the entry point called from an external source after the create method is called
  714. // - reload_settings This is called when the CLI option -r or --reload is used
  715. // - display_version
  716. // - command_options
  717. //
  718. // Using the class below use the commands
  719. // $simple_example = simple_example::create();
  720. // $simple_example->run();
  721. //
  722. // This will create the class and then run it once and exit with a success code.
  723. //
  724. //
  725. class simple_example extends service {
  726. protected function reload_settings(): void {
  727. }
  728. protected static function display_version(): void {
  729. echo "Version 1.00\n";
  730. }
  731. protected static function set_command_options() {
  732. }
  733. public function run(): int {
  734. echo "Successfully ran child service\n";
  735. echo "Try command line options -h or -v\n";
  736. return 0;
  737. }
  738. }
  739. //*/
  740. /*
  741. //
  742. // This class is more complex in that it will continue to run with a connection to a database
  743. //
  744. // The service class is divided between static and non-static methods. The static methods are
  745. // used and called before the service is run allowing the CLI options to be read and parsed
  746. // before the object is initialized. This allows for configuration options to be available
  747. // when the child class is first started up. Keep in mind that these are called statically
  748. // so that all callback functions declared in the cli options must be static.
  749. //
  750. class child_service extends service {
  751. //
  752. // Using a version constant is ideal for tracking and reporting
  753. //
  754. const CHILD_SERVICE_VERSION = '1.00';
  755. //
  756. // The parent service does not create a database connection as the child service may not need it. This example
  757. // demonstrates how the config object is passed from the parent and then used in the child service to connect
  758. // to other resources or use other settings the base class loaded so the child class automatically inherits.
  759. //
  760. private $database;
  761. // This example uses a settings object to demonstrate how the config is passed through to the child class
  762. // and is then used again in the reload_settings to demonstrate how the settings could be reloaded
  763. // with changes in the configuration, database connection, and default settings without the need to create
  764. // new instances of the config object.
  765. private $settings;
  766. //
  767. // This function is required from the base service class because it is used when the reload command line option is used
  768. //
  769. protected function reload_settings(): void {
  770. //informing the user in this example is simple but can use the parent class log functions
  771. echo "Reloading settings\n";
  772. //
  773. // Reload the configuration file
  774. //
  775. self::$config->read();
  776. //
  777. // If services have their own configuration file that was passed in using the -c or --config option, the options
  778. // would be available here as well to the child class
  779. // By allowing the config file to be specified, it is possible for services to have a configuration specific to them
  780. // while it could still be possible to allow access to the original making it very flexible with a wide degree of
  781. // choices.
  782. //
  783. // For example, specifying a configuration file that could be used for an archive or backup server would allow
  784. // the backup service to connect to another system remotely.
  785. //
  786. // It could also be used to separate the web configuration from system services to keep them organized and allow for
  787. // configuration settings to be available should the database fail. One possible scenario where this could be useful
  788. // is to send an email if the database stops responding. Currently, this is not possible as the database class uses
  789. // the 'die' command to immediately exit. I think it would be good to remove that and instead set the error message
  790. // to be something that would reflect the error allowing a system service to detect and even possibly correct that.
  791. //
  792. $alert_email = self::$config->get('alert_email', '');
  793. $smtp_host = self::$config->get('smtp_host', '');
  794. $smtp_port = self::$config->get('smtp_port', '');
  795. //
  796. // Ensure the database is connected with the new configuration parameters
  797. //
  798. $this->database->connect();
  799. //
  800. // The reload settings here completes the chain
  801. //
  802. $this->settings->reload();
  803. }
  804. //
  805. // This run function is required as it is called to launch child_service. This
  806. // is the entry point for the child class.
  807. //
  808. public function run(): int {
  809. //
  810. // Create the database object once passing a reference to the config object
  811. //
  812. $this->database = new database(['config' => self::$config]);
  813. //
  814. // Create the settings object using the database connection
  815. //
  816. $this->settings = new settings(['database' => $this->database]);
  817. //
  818. // In this example I have used the reload_settings because it is required by the parent class
  819. // whenever the '-r' or '--reload' option is given on the CLI. The base class is responsible for
  820. // parsing the information given on the CLI. Whenever the base class detects a '-r' option, the
  821. // reload_settings method in the child class is called. This gives the responsibility to the the
  822. // child class to reload any settings that might be needed during long execution of the service
  823. // without stopping and starting the service. The method is called here to initialize any and all
  824. // objects within the child service.
  825. //
  826. $this->reload_settings();
  827. //
  828. // The $running property is declared in the base service class as a boolean and it is responsible
  829. // to enable this so that the child class can run. The base service class will set this to false
  830. // if it receives a shutdown command from either the OS, PHP, or a posix signal allowing the child
  831. // class to respond or clean up after the while loop.
  832. //
  833. while($this->running) {
  834. //
  835. // This is where the actual heart of the code for the new service will be created
  836. //
  837. echo "Doing something..." . date("Y-m-d H:i:s") . "\n";
  838. sleep(1);
  839. }
  840. //
  841. // Returning a non-zero value would indicate there was an issue. Here we return zero to indicate graceful shutdown.
  842. //
  843. return 0;
  844. }
  845. //
  846. // This is the version that will be displayed when the option '-v' or '--version' is used on the command line.
  847. // This run function is required
  848. //
  849. protected static function display_version(): void {
  850. echo "Child service example version " . self::CHILD_SERVICE_VERSION . "\n";
  851. }
  852. //
  853. // set_command_options can either add to or replace options. Replacing the base options would allow an override for default behaviour.
  854. // This run function is required
  855. //
  856. protected static function set_command_options() {
  857. //
  858. // The options below are added to the CLI options and displayed whenever the -h or --help option is used.
  859. // There are multiple methods are used to suite the style of the creator
  860. //
  861. //
  862. // The callbacks set here are used to demonstrate multiple calls can be used
  863. //
  864. //using the parameter in the function
  865. self::add_command_option(
  866. 't:'
  867. , 'template:'
  868. , 'Full path and file name of the template file to use'
  869. , '-t <path>'
  870. , '--template <path>'
  871. , ['set_template_path']
  872. );
  873. //using a container object
  874. self::append_command_option(command_option::new()
  875. ->short_option('n')
  876. ->long_option('null')
  877. ->description('This option is to demonstrate using a cli object to create cli options')
  878. ->functions(['null_function_method'])
  879. );
  880. //using an array of key/value pairs
  881. self::append_command_option(command_option::new([
  882. 'short_option' => 'z:'
  883. ,'long_option' => 'zero:'
  884. ,'description' => 'This has zero effect on behavior'
  885. ,'function' => 'call_single_function'
  886. ]));
  887. //
  888. // These options are here but are commented out to allow the functionality to still exist in the parent
  889. //
  890. //
  891. // //replace cli options in the parent class using array
  892. // $index = 0;
  893. // $arr_options = [];
  894. // $arr_options[$index]['short_option'] = 'z';
  895. // $arr_options[$index]['long_option'] = 'zero';
  896. // $arr_options[$index]['description'] = 'This has zero effect on behavior';
  897. // $arr_options[$index]['short_description'] = '-z';
  898. // $arr_options[$index]['long_description'] = '--zero';
  899. // $arr_options[$index]['function'][] = 'call_single_function';
  900. // self::$available_command_options = $arr_options;
  901. //
  902. // //replace all cli options using container object
  903. // $arr_options = [];
  904. // self::$available_command_options = [];
  905. // $arr_options[0] = command_option::new()
  906. // ->short_option('z')
  907. // ->short_description('-z')
  908. // ->function('call_a_function')
  909. // ->function('call_another_function_after_first')
  910. // ->description('This option does nothing')
  911. // ->to_array();
  912. //
  913. // $arr_options[1] = command_option::new([
  914. // 'short_option' => 'z'
  915. // ,'long_option' => '--zero'
  916. // ,'description' => 'This option does nothing'
  917. // ,'functions' => ['call_a_function', 'call_another_function']
  918. // ])->to_array();
  919. //self::$available_command_options = $arr_options;
  920. }
  921. } // class child_service
  922. //*/
  923. /*
  924. //
  925. // Standard includes do not apply for the base class because the require.php has included many other php files. These other files
  926. // or objects may not be required for some services. Thus, only the config is required for base_service. Child services may then
  927. // create a database class and use it by passing the config object to the database constructor. This is why the 'require.php' is
  928. // left out of the initial setup class.
  929. //
  930. // Use the auto_loader to find any classes needed so we don't have a lot of include statements
  931. // In this example, the auto_loader should not be using the PROJECT_ROOT or any other defined constants
  932. // because they are not needed in the initial stage of loading
  933. require_once __DIR__ . '/auto_loader.php';
  934. // We don't need to ever reference the object so don't assign a variable. It
  935. // would be a good idea to remove the auto_loader as a class declaration so
  936. // that there would only need to be one line. It seems illogical to have an
  937. // object that never needs to be referenced.
  938. new auto_loader();
  939. // The base_service class has a 'protected' constructor, meaning you are not able to use "new" to create the object. Instead, you
  940. // must use the 'create' static method to create an object. This technique is employed because some PHP versions have an issue with
  941. // registering signal listeners in the constructor. See the link https://www.php.net/manual/en/function.pcntl-signal.php in the user
  942. // comments section.
  943. // The child_service class does not override the parent constructor so parent constructor is used. If the child_service class does
  944. // have a constructor then the child class must call:
  945. // parent::__construct($config);
  946. // as the first line of the child constructor. This is because the parent constructor uses the config class. This also means
  947. // that the child class must receive the config object in the constructor as a minimum.
  948. $service = child_service::create();
  949. // The run class is declared as abstract in the parent. So the child class must have one.
  950. $service->run();
  951. //*/