aboutsummaryrefslogtreecommitdiff
path: root/lib/classes/CronjobScheduler.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/classes/CronjobScheduler.php')
-rw-r--r--lib/classes/CronjobScheduler.php333
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');
+ }
+}