diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/classes/CronjobScheduler.php | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/classes/CronjobScheduler.php')
| -rw-r--r-- | lib/classes/CronjobScheduler.php | 333 |
1 files changed, 333 insertions, 0 deletions
diff --git a/lib/classes/CronjobScheduler.php b/lib/classes/CronjobScheduler.php new file mode 100644 index 0000000..fbb5b66 --- /dev/null +++ b/lib/classes/CronjobScheduler.php @@ -0,0 +1,333 @@ +<?php +/** + * CronjobScheduler - Scheduler for the cronjobs. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @since 2.4 + */ + +// +---------------------------------------------------------------------------+ +// This file is part of Stud.IP +// CronjobScheduler.php +// +// Copyright (C) 2013 Jan-Hendrik Willms <tleilax+studip@gmail.com> +// +---------------------------------------------------------------------------+ +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or any later version. +// +---------------------------------------------------------------------------+ +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +---------------------------------------------------------------------------+ + +class CronjobScheduler +{ + protected static $instance = null; + + /** + * Returns the scheduler object. Implements the singleton pattern to + * ensure that only one scheduler exists. + * + * @return CronjobScheduler The scheduler object + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Private constructor to ensure the singleton pattern is used correctly. + */ + private function __construct() + { + } + + /** + * Registers a new executable task. + * + * @param mixed $task Either path of the task class filename (relative + * to Stud.IP root) or an instance of CronJob + * @param bool $active Indicates whether the task should be set active + * or not + * @return String Id of the created task + * @throws InvalidArgumentException when the task class file does not + * exist + * @throws RuntimeException when task has already been registered + */ + public function registerTask($task, $active = true) + { + if (is_object($task)) { + $reflection = new ReflectionClass($task); + $class = $reflection->getName(); + $class_filename = studip_relative_path($reflection->getFileName()); + } else { + $filename = $GLOBALS['STUDIP_BASE_PATH'] . '/' . $task; + if (!file_exists($filename)) { + $message = sprintf('Task class file "%s" does not exist.', $task); + throw new InvalidArgumentException($message); + } + $class_filename = $task; + + $classes = get_declared_classes(); + require_once $filename; + $new_classes = array_diff(get_declared_classes(), $classes); + $new_classes = array_filter($new_classes, function ($class) { + return is_subclass_of($class, 'CronJob', true); + }); + $class = end($new_classes); + + if (empty($class)) { + throw new RuntimeException('No valid class was defined in file.'); + } + + $reflection = new ReflectionClass($class); + } + + if (!$reflection->isSubclassOf('CronJob')) { + $message = sprintf('Job class "%s" (defined in %s) does not extend the abstract CronJob class.', $class, $filename); + throw new RuntimeException($message); + } + + if ($task = CronjobTask::findOneByClass($class)) { + return $task->task_id; + } + + $task = new CronjobTask(); + $task->filename = $class_filename; + $task->class = $class; + $task->active = (int)$active; + $task->store(); + + return $task->task_id; + } + + /** + * Unregisters a previously registered task. + * + * @param String $task_id Id of the task to be unregistered + * @return CronjobScheduler to allow chaining + * @throws InvalidArgumentException when no task with the given id exists + */ + public function unregisterTask($task_id) + { + $task = CronjobTask::find($task_id); + if ($task === null) { + $message = sprintf('A task with the id "%s" does not exist.', $task_id); + throw new InvalidArgumentException($message); + } + $task->delete(); + + return $this; + } + + /** + * Schedules a task for periodic execution with the provided schedule. + * + * @param String $task_id The id of the task to be executed + * @param mixed $minute Minute part of the schedule: + * - null for "every minute" a.k.a. "don't care" + * - x < 0 for "every x minutes" + * - x >= 0 for "only at minute x" + * @param mixed $hour Hour part of the schedule: + * - null for "every hour" a.k.a. "don't care" + * - x < 0 for "every x hours" + * - x >= 0 for "only at hour x" + * @param mixed $day Day part of the schedule: + * - null for "every day" a.k.a. "don't care" + * - x < 0 for "every x days" + * - x > 0 for "only at day x" + * @param mixed $month Month part of the schedule: + * - null for "every month" a.k.a. "don't care" + * - x < 0 for "every x months" + * - x > 0 for "only at month x" + * @param mixed $day_of_week Day of week part of the schedule: + * - null for "every day" a.k.a. "don't care" + * - 1 >= x >= 7 for "exactly at day of week x" + * (x starts with monday at 1 and ends with + * sunday at 7) + * @param Array $parameters Optional parameters passed to the task + * @return CronjobSchedule The generated schedule object. + */ + public function schedule( + string $task_id, + ?int $minute = null, + ?int $hour = null, + ?int $day = null, + ?int $month = null, + ?int $day_of_week = null, + array $parameters = [] + ): CronjobSchedule { + $schedule = new CronjobSchedule(); + $schedule->task_id = $task_id; + $schedule->parameters = $parameters; + + $schedule->minute = $minute; + $schedule->hour = $hour; + $schedule->day = $day; + $schedule->month = $month; + $schedule->day_of_week = $day_of_week; + + $schedule->store(); + + $task = $schedule->task; + $task->assigned_count += 1; + $task->store(); + + return $schedule; + } + + /** + * An alias for schedule for backwards compatibility. + * + * @see CronjobScheduler::schedule() + */ + public function schedulePeriodic( + $task_id, + $minute = null, + $hour = null, + $day = null, + $month = null, + $day_of_week = null, + $priority = null, + $parameters = [] + ) { + return $this->schedule($task_id, $minute, $hour, $day, $month, $day_of_week, $parameters); + } + + /** + * Cancels the provided schedule. + * + * @param String $schedule_id Id of the schedule to be canceled + */ + public function cancel($schedule_id) + { + CronjobSchedule::find($schedule_id)->delete(); + } + + /** + * Cancels all schedules of the provided task. + * + * @param String $task_id Id of the task which schedules shall be canceled + */ + public function cancelByTask($task_id) + { + $schedules = CronjobSchedule::findByTask_id($task_id); + foreach ($schedules as $schedule) { + $schedule->delete(); + } + } + + /** + * Executes the available schedules if they are to be executed. + * This method can only be run once - even if one execution takes more + * than planned. This is ensured by a locking mechanism. + */ + public function run() + { + if (!Config::get()->CRONJOBS_ENABLE) { + return; + } + + $lock = new FileLock('studip-cronjob'); + + // Check whether a previous cronjob worker is still running. + if (!$lock->tryLock()) { + return; + } + + // Find all schedules that are due to execute and which task is active + $temp = CronjobSchedule::findBySQL('`active` = 1 AND `next_execution` <= UNIX_TIMESTAMP() ' + .'ORDER BY `next_execution`'); + $schedules = array_filter($temp, function ($schedule) { return $schedule->task->active; }); + + if (count($schedules) === 0) { + return; + } + + foreach ($schedules as $schedule) { + $log = new CronjobLog(); + $log->schedule_id = $schedule->schedule_id; + $log->scheduled = $schedule->next_execution; + $log->executed = time(); + $log->exception = null; + $log->duration = -1; + + try { + // Skip schedules with missing task classes + if (!$schedule->task->valid) { + throw new Exception(_('Die Klasse für den Cronjob-Task konnte nicht gefunden werden')); + } + + // Start capturing output and measuring duration + ob_start(); + $start_time = microtime(true); + + $schedule->execute(); + + // Actually capture output and duration + $end_time = microtime(true); + $output = ob_get_clean(); + + // Complete log + $log->output = $output; + $log->duration = $end_time - $start_time; + $log->store(); + } catch (Exception $e) { + $log->exception = $e; + $log->store(); + + // Deactivate schedule + $schedule->deactivate(); + + // Send mail to root accounts + $subject = sprintf('[Cronjobs] %s: %s', + _('Fehlerhafte Ausführung'), + $schedule->title); + + $message = sprintf(_('Der Cronjob "%s" wurde deaktiviert, da bei der Ausführung ein Fehler aufgetreten ist.'), $schedule->title) . "\n"; + $message .= "\n"; + $message .= display_exception($e) . "\n"; + + $message .= _('Für weiterführende Informationen klicken Sie bitten den folgenden Link:') . "\n"; + + $old = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']); + $message .= URLHelper::getURL('dispatch.php/admin/cronjobs/logs/schedule/' . $schedule->schedule_id); + URLHelper::setBaseURL($old); + + $this->sendMailToRoots($subject, $message); + } + } + + // Release lock + $lock->release(); + } + + /** + * Sends an internal mail with the provided subject and message to all + * users with a global permission of "root". + * + * @param String $subject The subject of the message + * @param String $message The message itself + */ + private function sendMailToRoots($subject, $message) + { + $temp = User::findByPerms('root'); + $roots = SimpleORMapCollection::createFromArray($temp) + ->filter(function($r) { return $r->locked == 0; }) + ->pluck('username'); + + $msging = new messaging; + $msging->insert_message($message, $roots, '____%system%____', null, null, null, null, $subject, false, 'high'); + } +} |
