maintenance_service.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  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. * Maintenance Service extends the service class to provide an intuitive way to manage files and database records
  29. * on the server. This allows the integration of the GUI and the system service. Once the service is started, the
  30. * changes made to the default settings will automatically take effect for the service without reloading. It is
  31. * possible to have the service handle quotas and complex tasks as long as the class to handle them has met the
  32. * following criteria:
  33. * 1. The class must be located in the app/(appname)/resources/classes folder
  34. * 2. The class must have a 'public static function database_maintenance(settings $settings): void' declaration
  35. * or a 'public static function filesystem_maintenance(settings $settings): void' declaration
  36. *
  37. * It is up to the class implementation to do any domain specific settings within the function.
  38. *
  39. * The interfaces of database_maintenance and filesystem_maintenance for classes are provided but are not necessary
  40. * for the behavior of maintenance services being executed.
  41. *
  42. * @author Tim Fry <[email protected]>
  43. */
  44. class maintenance_service extends service {
  45. const LOG_OK = 'ok';
  46. const LOG_ERROR = 'error';
  47. /**
  48. * Database object
  49. * @var database
  50. * @access private
  51. */
  52. private $database;
  53. /**
  54. * Settings object
  55. * @var settings
  56. * @access private
  57. */
  58. private $settings;
  59. /**
  60. * List of purges to perform
  61. * @var array
  62. * @access private
  63. */
  64. private $maintenance_apps;
  65. /**
  66. * Execution time for the maintenance to work at
  67. * @var string|null
  68. * @access private
  69. */
  70. private $execute_time;
  71. /**
  72. * Maintenance work will only be performed if this is set to true
  73. * @var bool
  74. * @access private
  75. */
  76. private $enabled;
  77. /**
  78. * Database object
  79. * @var database
  80. * @access private
  81. */
  82. private static $db = null;
  83. /**
  84. * Array of logs to write to database_maintenance table
  85. * @var array
  86. * @access private
  87. */
  88. private static $logs = null;
  89. /**
  90. * Integer to track the number of seconds to sleep between time match checking
  91. * @var int
  92. * @access private
  93. */
  94. private $check_interval;
  95. /**
  96. * Tracks if the immediate flag (-i or --immediate) was used on startup
  97. * @var bool True if the immediate flag was passed in on initial launch or false. Default is false.
  98. * @access private
  99. */
  100. private static $execute_on_startup = false;
  101. /**
  102. * Can extend the base cli options
  103. * @access protected
  104. */
  105. #[\Override]
  106. protected static function set_command_options() {
  107. //add a new command line option
  108. self::append_command_option(command_option::new()
  109. ->short_option('i')
  110. ->long_option('immediate')
  111. ->description('Launch maintenance tasks immediately on startup and on each reload')
  112. ->functions(['set_immediate'])
  113. );
  114. }
  115. /**
  116. * Show the version on the console when the -r or --version is used
  117. * @return void
  118. * @access protected
  119. */
  120. #[\Override]
  121. protected static function display_version(): void {
  122. echo "Version " . self::VERSION . "\n";
  123. }
  124. /**
  125. * This is called during the service creation if the -i or --immediate option is passed
  126. * @access protected
  127. */
  128. protected static function set_immediate() {
  129. self::$execute_on_startup = true;
  130. }
  131. /**
  132. * This is called whenever either a reload signal is received to the running instance or
  133. * when the cli option -r or --reload is given to the service.
  134. * This is also called when the maintainer_service is first created to connect to the database
  135. * and reload the settings from the global default settings
  136. * @return void
  137. * @access protected
  138. */
  139. #[\Override]
  140. protected function reload_settings(): void {
  141. //reload the config file
  142. self::$config->read();
  143. //re-connect the database just-in-case the config settings have changed
  144. $this->database->connect();
  145. //reload settings
  146. $this->settings->reload();
  147. //check if we are enabled to work or not
  148. $this->enabled = $this->settings->get('maintenance', 'enabled', false) === true ? true : false;
  149. //returns an array of maintenance applications
  150. $this->maintenance_apps = $this->settings->get('maintenance', 'application', []);
  151. //time of day to execute in 24-hour time format
  152. $this->execute_time = $this->settings->get('maintenance', 'time_of_day', null);
  153. //sleep seconds between tests for matching the current time to the execute time
  154. $this->check_interval = intval($this->settings->get('maintenance', 'check_interval', 33));
  155. self::log("Settings Reloaded", LOG_INFO);
  156. }
  157. /**
  158. * Non-zero values indicate that the service failed to start
  159. * @return void
  160. * @access public
  161. */
  162. #[\Override]
  163. public function run(): int {
  164. //log the startup
  165. self::log('Starting up...', LOG_INFO);
  166. //load functions
  167. require_once dirname(__DIR__, 4) . '/resources/functions.php';
  168. //set the database to use the config file
  169. $this->database = new database(['config' => static::$config]);
  170. //save the database for logging but object and static properties can't the same name
  171. self::$db = $this->database;
  172. //initialize the logs for the workers
  173. self::$logs = [];
  174. //set the settings to use the connected database
  175. $this->settings = new settings(['database' => $this->database]);
  176. //reload the default settings
  177. $this->reload_settings();
  178. //check for starting service exactly on the time needed
  179. if ($this->enabled && !empty($this->execute_time) && (date('H:i') == $this->execute_time || self::$execute_on_startup)) {
  180. $this->run_maintenance();
  181. }
  182. //main loop
  183. while ($this->running) {
  184. //allow the service to run but not do anything
  185. if ($this->enabled && !empty($this->execute_time)) {
  186. //wait until the time matches requested time of day
  187. do {
  188. $server_time = date('H:i');
  189. // check more than once a minute
  190. sleep($this->check_interval);
  191. } while ($this->execute_time <> $server_time && $this->running);
  192. //reload settings before executing the tasks to capture changes
  193. $this->reload_settings();
  194. //check that we are still enabled after reload before we execute again
  195. if ($this->enabled && !empty($this->execute_time)) {
  196. //run all registered apps
  197. $this->run_maintenance();
  198. }
  199. } else {
  200. //wait five minutes and reload to see if we are enabled
  201. sleep(300);
  202. $this->reload_settings();
  203. }
  204. }
  205. return 0;
  206. }
  207. /**
  208. * Executes the maintenance for both database and filesystem objects using their respective helper methods
  209. * @access protected
  210. */
  211. protected function run_maintenance() {
  212. //get the registered apps
  213. $apps = $this->settings->get('maintenance', 'application', []);
  214. foreach ($apps as $app) {
  215. //execute all database maintenance applications
  216. if (method_exists($app, 'database_maintenance')) {
  217. $app::database_maintenance($this->settings);
  218. }
  219. //execute all filesystem maintenance applications
  220. if (method_exists($app, 'filesystem_maintenance')) {
  221. $app::filesystem_maintenance($this->settings);
  222. }
  223. }
  224. //write only once to database maintainance logs and flush the array
  225. self::log_flush();
  226. }
  227. /**
  228. * Write any pending transactions to the database
  229. * @access public
  230. */
  231. public static function log_flush() {
  232. //ensure the log_flush is not used to hijack the log_write function
  233. if (self::$logs !== null && count(self::$logs) > 0) {
  234. $array['maintenance_logs'] = self::$logs;
  235. //write to the database
  236. self::$db->save($array, false);
  237. //write each log entry to syslog
  238. foreach (self::$logs as $log_entry) {
  239. $message = "domain=" . $log_entry['domain_uuid']
  240. . ", application=" . $log_entry['maintenance_log_application']
  241. . ", message=" . $log_entry['maintenance_log_message']
  242. . ", status=" . $log_entry['maintenance_log_status'];
  243. self::log($message);
  244. }
  245. //clear the log queue
  246. self::$logs = [];
  247. }
  248. }
  249. ////////////////////////////////////////////////////
  250. // Common functions used with maintainer services //
  251. ////////////////////////////////////////////////////
  252. /**
  253. * Saves the logs in an array in order to write them all at once. This is to remove the number of times the database will try to
  254. * be written to during the many worker processes to improve performance similar to an atomic commit.
  255. * @param database_maintenance|filesystem_maintenance|string $worker_or_classname
  256. * @param string $message Message to put in the log
  257. * @param string|null $domain_uuid UUID of the domain that applies or null (default)
  258. * @param string $status LOG_OK (default) or LOG_ERROR
  259. * @access public
  260. */
  261. public static function log_write($worker_or_classname, string $message, ?string $domain_uuid = null, string $status = self::LOG_OK) {
  262. require_once dirname(__DIR__) . '/functions.php';
  263. $classname = get_classname($worker_or_classname);
  264. //protect against hijacking the log writer
  265. if (self::$logs !== null) {
  266. $row_index = count(self::$logs);
  267. self::$logs[$row_index]['domain_uuid'] = $domain_uuid;
  268. self::$logs[$row_index]['maintenance_log_uuid'] = uuid();
  269. self::$logs[$row_index]['maintenance_log_application'] = $classname;
  270. self::$logs[$row_index]['maintenance_log_epoch'] = time();
  271. self::$logs[$row_index]['maintenance_log_message'] = $message;
  272. self::$logs[$row_index]['maintenance_log_status'] = $status;
  273. //only allow up to 100 entries before saving to the database
  274. if (count(self::$logs) > 100) {
  275. self::log_flush();
  276. }
  277. }
  278. }
  279. /**
  280. * Returns a list of domains with the domain_uuid as the key and the domain_name as the value
  281. * @param database $database
  282. * @param bool $ignore_domain_enabled Omit the where clause for domain_enabled
  283. * @param bool $domain_status When the <code>$ignore_domain_enabled</code> is false, set the status to true or false
  284. * @return array Domain uuid as key and domain name as value
  285. * @access public
  286. */
  287. public static function get_domains(database $database, bool $ignore_domain_enabled = false, bool $domain_status = true): array {
  288. $domains = [];
  289. $status_string = $domain_status ? 'true' : 'false';
  290. $sql = "select domain_uuid, domain_name from v_domains";
  291. if (!$ignore_domain_enabled) {
  292. $sql .= " where domain_enabled='$status_string'";
  293. }
  294. $result = $database->select($sql);
  295. if (!empty($result)) {
  296. foreach ($result as $row) {
  297. $domains[$row['domain_uuid']] = $row['domain_name'];
  298. }
  299. }
  300. return $domains;
  301. }
  302. /**
  303. * Returns the number of seconds passed since the file was modified
  304. * @param string $file Full path and file name
  305. * @return int number of seconds since the file was modified
  306. * @depends filemtime
  307. * @access public
  308. */
  309. public static function seconds_since_modified(string $file): int {
  310. //check the file date and time
  311. return floor(time() - filemtime($file));
  312. }
  313. /**
  314. * Returns the number of seconds passed since the file was created
  315. * @param string $file Full path and file name
  316. * @return int number of seconds since the file was created
  317. * @depends filectime
  318. * @access public
  319. */
  320. public static function seconds_since_created(string $file): int {
  321. //check the file date and time
  322. return floor(time() - filectime($file));
  323. }
  324. /**
  325. * Returns the number of minutes passed since the file was created
  326. * @param string $file Full path and file name
  327. * @return int number of minutes since the file was created
  328. * @depends seconds_since_created
  329. * @access public
  330. */
  331. public static function minutes_since_created(string $file): int {
  332. return floor(self::seconds_since_created($file) / 60);
  333. }
  334. /**
  335. * Returns the number of minutes passed since the file was modified
  336. * @param string $file Full path and file name
  337. * @return int number of minutes since the file was modified
  338. * @depends seconds_since_modified
  339. * @access public
  340. */
  341. public static function minutes_since_modified(string $file): int {
  342. return floor(self::seconds_since_modified($file) / 60);
  343. }
  344. /**
  345. * Returns the number of hours passed since the file was created
  346. * @param string $file Full path and file name
  347. * @return int number of hours since the file was created
  348. * @depends minutes_since_create
  349. * @access public
  350. */
  351. public static function hours_since_created(string $file): int {
  352. return floor(self::minutes_since_created($file) / 60);
  353. }
  354. /**
  355. * Returns the number of hours passed since the file was modified
  356. * @param string $file Full path and file name
  357. * @return int number of hours since the file was modified
  358. * @depends minutes_since_create
  359. * @access public
  360. */
  361. public static function hours_since_modified(string $file): int {
  362. return floor(self::minutes_since_modified($file) / 60);
  363. }
  364. /**
  365. * Returns the number of days passed since the file was created
  366. * @param string $file Full path and file name
  367. * @return int number of days since the file was created
  368. * @depends hours_since_created
  369. * @access public
  370. */
  371. public static function days_since_created(string $file): int {
  372. return floor(self::hours_since_created($file) / 24);
  373. }
  374. /**
  375. * Returns the number of days passed since the file was modified
  376. * @param string $file Full path and file name
  377. * @return int number of days since the file was modified
  378. * @depends hours_since_modified
  379. * @access public
  380. */
  381. public static function days_since_modified(string $file): int {
  382. return floor(self::hours_since_modified($file) / 24);
  383. }
  384. /**
  385. * Returns the number of months passed since the file was created. Based on a month having 30 days.
  386. * @param string $file Full path and file name
  387. * @return int number of months since the file was created
  388. * @depends days_since_created
  389. * @access public
  390. */
  391. public static function months_since_created(string $file): int {
  392. return floor(self::days_since_created($file) / 30);
  393. }
  394. /**
  395. * Returns the number of months passed since the file was modified. Based on a month having 30 days.
  396. * @param string $file Full path and file name
  397. * @return int number of months since the file was modified
  398. * @depends days_since_modified
  399. * @access public
  400. */
  401. public static function months_since_modified(string $file): int {
  402. return floor(self::days_since_modified($file) / 30);
  403. }
  404. /**
  405. * Returns the number of weeks passed since the file was created
  406. * @param string $file Full path and file name
  407. * @return int number of weeks since the file was created
  408. * @depends days_since_created
  409. * @access public
  410. */
  411. public static function weeks_since_created(string $file): int {
  412. return floor(self::days_since_created($file) / 7);
  413. }
  414. /**
  415. * Returns the number of weeks passed since the file was modified
  416. * @param string $file Full path and file name
  417. * @return int number of weeks since the file was modified
  418. * @depends days_since_modified
  419. * @access public
  420. */
  421. public static function weeks_since_modified(string $file): int {
  422. return floor(self::days_since_modified($file) / 7);
  423. }
  424. /**
  425. * Returns the number of years passed since the file was created
  426. * @param string $file Full path and file name
  427. * @return int number of years since the file was created
  428. * @depends weeks_since_created
  429. * @access public
  430. */
  431. public static function years_since_created(string $file): int {
  432. return floor(self::weeks_since_created($file) / 52);
  433. }
  434. /**
  435. * Returns the number of years passed since the file was modified
  436. * @param string $file Full path and file name
  437. * @return int number of years since the file was modified
  438. * @depends weeks_since_modified
  439. * @access public
  440. */
  441. public static function years_since_modified(string $file): int {
  442. return floor(self::weeks_since_modified($file) / 52);
  443. }
  444. }