Skip to content
Snippets Groups Projects
Select Git revision
  • e05f5e6273af854a24a7fae1a19ea57ecfe7c68e
  • main default protected
  • step-3263
  • feature/plugins-cli
  • feature/vite
  • step-2484-peerreview
  • biest/issue-5051
  • tests/simplify-jsonapi-tests
  • fix/typo-in-1a70031
  • feature/broadcasting
  • database-seeders-and-factories
  • feature/peer-review-2
  • feature-feedback-jsonapi
  • feature/peerreview
  • feature/balloon-plus
  • feature/stock-images-unsplash
  • tic-2588
  • 5.0
  • 5.2
  • biest/unlock-blocks
  • biest-1514
21 results

CronjobScheduler.class.php

Blame
  • Forked from Stud.IP / Stud.IP
    Source project has a limited visibility.
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    CronjobScheduler.class.php 12.74 KiB
    <?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 a single execution at the provided time.
         *
         * @param String $task_id    The id of the task to be executed
         * @param int    $timestamp  When the task should be executed
         * @param String $priority   Priority of the execution (low, normal, high),
         *                           defaults to normal
         * @param Array  $parameters Optional parameters passed to the task
         * @return CronjobSchedule The generated schedule object.
         */
        public function scheduleOnce($task_id, $timestamp, $priority = CronjobSchedule::PRIORITY_NORMAL,
                                     $parameters = [])
        {
            $schedule = new CronjobSchedule();
            $schedule->type           = 'once';
            $schedule->task_id        = $task_id;
            $schedule->parameters     = $parameters;
            $schedule->priority       = $priority;
            $schedule->next_execution = $timestamp;
    
            $schedule->store();
    
            $task = $schedule->task;
            $task->assigned_count += 1;
            $task->store();
    
            return $schedule;
        }
    
        /**
         * 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 String $priority   Priority of the execution (low, normal, high),
         *                           defaults to normal
         * @param Array  $parameters Optional parameters passed to the task
         * @return CronjobSchedule The generated schedule object.
         */
        public function schedulePeriodic($task_id, $minute = null, $hour = null,
                                         $day = null, $month = null, $day_of_week = null,
                                         $priority = CronjobSchedule::PRIORITY_NORMAL,
                                         $parameters = [])
        {
            $schedule = new CronjobSchedule();
            $schedule->type       = 'periodic';
            $schedule->task_id    = $task_id;
            $schedule->parameters = $parameters;
            $schedule->priority   = $priority;
    
            $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;
        }
    
        /**
         * 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 priority DESC, next_execution ASC');
            $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');
        }
    }