<?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.class.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'); } }