Skip to content
Snippets Groups Projects
CronjobScheduler.class.php 11.9 KiB
Newer Older
<?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.
        }

        // 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) {
            try {
                // Skip schedules with missing task classes
                if (!$schedule->task->valid) {
David Siegfried's avatar
David Siegfried committed
                    throw new Exception(_('Die Klasse für den Cronjob-Task konnte nicht gefunden werden'));
                }

                $log = new CronjobLog();
                $log->schedule_id = $schedule->schedule_id;
                $log->scheduled   = $schedule->next_execution;
                $log->executed    = time();
                $log->exception   = null;
                $log->duration    = -1;
                $log->store();

                // 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;

                // 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
    }

    /**
     * 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');
    }
}