MDL-55980 Scheduled tasks: Run individual scheduled tasks from web
authorsam marshall <s.marshall@open.ac.uk>
Mon, 26 Sep 2016 16:37:04 +0000 (17:37 +0100)
committersam marshall <s.marshall@open.ac.uk>
Fri, 24 Feb 2017 10:42:28 +0000 (10:42 +0000)
admin/settings/security.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/schedule_task.php [new file with mode: 0644]
admin/tool/task/styles.css
admin/tool/task/templates/link_back.mustache [new file with mode: 0644]
admin/tool/task/tests/behat/run_task_now.feature [new file with mode: 0644]
lib/cronlib.php
lib/moodlelib.php
lib/sessionlib.php

index 793205d..c36a424 100644 (file)
@@ -59,6 +59,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('cronclionly', new lang_string('cronclionly', 'admin'), new lang_string
             ('configcronclionly', 'admin'), 1));
     $temp->add(new admin_setting_configpasswordunmask('cronremotepassword', new lang_string('cronremotepassword', 'admin'), new lang_string('configcronremotepassword', 'admin'), ''));
+    $temp->add(new admin_setting_configcheckbox('tool_task/enablerunnow', new lang_string('enablerunnow', 'tool_task'),
+            new lang_string('enablerunnow_desc', 'tool_task'), 1));
 
     $options = array(0=>get_string('no'), 3=>3, 5=>5, 7=>7, 10=>10, 20=>20, 30=>30, 50=>50, 100=>100);
     $temp->add(new admin_setting_configselect('lockoutthreshold', new lang_string('lockoutthreshold', 'admin'), new lang_string('lockoutthreshold_desc', 'admin'), 0, $options));
index fa6fe77..593ea98 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 $string['asap'] = 'ASAP';
+$string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
 $string['component'] = 'Component';
 $string['corecomponent'] = 'Core';
@@ -30,6 +31,8 @@ $string['default'] = 'Default';
 $string['disabled'] = 'Disabled';
 $string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
+$string['enablerunnow'] = 'Allow &lsquo;Run now&rsquo; for scheduled tasks';
+$string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The task runs on the web server, so some sites may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
 $string['lastruntime'] = 'Last run';
 $string['nextruntime'] = 'Next run';
@@ -37,6 +40,8 @@ $string['plugindisabled'] = 'Plugin disabled';
 $string['pluginname'] = 'Scheduled task configuration';
 $string['resettasktodefaults'] = 'Reset task schedule to defaults';
 $string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
+$string['runnow'] = 'Run now';
+$string['runnow_confirm'] = 'Are you sure you want to run this task &lsquo;{$a}&rsquo; now? The task will run on the web server and may take some time to complete.';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
index 076f092..575aa66 100644 (file)
@@ -104,11 +104,19 @@ class tool_task_renderer extends plugin_renderer_base {
                 $nextrun = $asap;
             }
 
+            $runnow = '';
+            if (!$disabled && get_config('tool_task', 'enablerunnow')) {
+                $runnow = html_writer::div(html_writer::link(
+                        new moodle_url('/admin/tool/task/schedule_task.php',
+                            array('task' => get_class($task))),
+                        get_string('runnow', 'tool_task')), 'task-runnow');
+            }
+
             $row = new html_table_row(array(
                         $namecell,
                         $componentcell,
                         new html_table_cell($editlink),
-                        new html_table_cell($lastrun),
+                        new html_table_cell($lastrun . $runnow),
                         new html_table_cell($nextrun),
                         new html_table_cell($task->get_minute()),
                         new html_table_cell($task->get_hour()),
@@ -133,4 +141,14 @@ class tool_task_renderer extends plugin_renderer_base {
         $table->data = $data;
         return html_writer::table($table);
     }
+
+    /**
+     * Renders a link back to the scheduled tasks page (used from the 'run now' screen).
+     *
+     * @return string HTML code
+     */
+    public function link_back() {
+        return $this->render_from_template('tool_task/link_back',
+                array('url' => new moodle_url('/admin/tool/task/scheduledtasks.php')));
+    }
 }
diff --git a/admin/tool/task/schedule_task.php b/admin/tool/task/schedule_task.php
new file mode 100644 (file)
index 0000000..53136f2
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Web cron single task
+ *
+ * This script runs a single scheduled task from the web UI.
+ *
+ * @package tool_task
+ * @copyright 2016 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require('../../../config.php');
+
+require_once($CFG->libdir.'/cronlib.php');
+
+/**
+ * Function used to handle mtrace by outputting the text to normal browser window.
+ *
+ * @param string $message Message to output
+ * @param string $eol End of line character
+ */
+function tool_task_mtrace_wrapper($message, $eol) {
+    echo s($message . $eol);
+    // Both types of flush may be necessary in order to actually output progressively to browser.
+    // It depends on the theme.
+    if (ob_get_status()) {
+        ob_flush();
+    }
+    flush();
+}
+
+// Allow execution of single task. This requires login and has different rules.
+$taskname = required_param('task', PARAM_RAW_TRIMMED);
+
+// Basic security checks.
+require_login();
+$context = context_system::instance();
+require_capability('moodle/site:config', $context);
+
+if (!get_config('tool_task', 'enablerunnow')) {
+    print_error('nopermissions', 'error', '', get_string('runnow', 'tool_task'));
+}
+
+// Check input parameter against all existing tasks (this ensures it isn't possible to
+// create some kind of security problem by specifying a class that isn't a task or whatever).
+$task = \core\task\manager::get_scheduled_task($taskname);
+if (!$task) {
+    print_error('cannotfindinfo', 'error', $taskname);
+}
+
+// Start output.
+$PAGE->set_url(new moodle_url('/admin/tool/task/schedule_task.php'));
+$PAGE->set_context($context);
+$PAGE->navbar->add(get_string('scheduledtasks', 'tool_task'), new moodle_url('/admin/tool/task/scheduledtasks.php'));
+$PAGE->navbar->add(s($task->get_name()));
+echo $OUTPUT->header();
+echo $OUTPUT->heading($task->get_name());
+
+// The initial request just shows the confirmation page; we don't do anything further unless
+// they confirm.
+if (!optional_param('confirm', 0, PARAM_INT)) {
+    echo $OUTPUT->confirm(get_string('runnow_confirm', 'tool_task', $task->get_name()),
+            new single_button(new moodle_url('/admin/tool/task/schedule_task.php',
+            array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+            get_string('runnow', 'tool_task')),
+            new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'),
+            get_string('cancel'), false));
+    echo $OUTPUT->footer();
+    exit;
+}
+
+// Action requires session key.
+require_sesskey();
+
+// Prepare to handle output via mtrace.
+echo html_writer::start_tag('pre');
+$CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
+
+// Run the specified task (this will output an error if it doesn't exist).
+cron_run_single_task($task);
+echo html_writer::end_tag('pre');
+
+$output = $PAGE->get_renderer('tool_task');
+echo $output->link_back();
+
+echo $OUTPUT->footer();
index 8aca57c..7714674 100644 (file)
@@ -9,3 +9,7 @@
     /*rtl:ignore*/
     direction: ltr;
 }
+
+#page-admin-tool-task-scheduledtasks .task-runnow {
+    font-size: 0.75em;
+}
diff --git a/admin/tool/task/templates/link_back.mustache b/admin/tool/task/templates/link_back.mustache
new file mode 100644 (file)
index 0000000..b6bd324
--- /dev/null
@@ -0,0 +1,31 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle 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 3 of the License, or
+    (at your option) any later version.
+
+    Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_task/link_back
+
+    Template for displaying link back from another page to the scheduled tasks page.
+
+    Context variables required for this template:
+    * url - moodle_url|string URL of scheduled tasks page
+
+    Example context (json):
+    {
+        "url" : "https://www.example.org/admin/tool/task/scheduledtasks.php"
+    }
+
+}}
+<p><a href="{{url}}">{{# str }} backtoscheduledtasks, tool_task {{/str}}</a></p>
diff --git a/admin/tool/task/tests/behat/run_task_now.feature b/admin/tool/task/tests/behat/run_task_now.feature
new file mode 100644 (file)
index 0000000..4740732
--- /dev/null
@@ -0,0 +1,35 @@
+@tool @tool_task
+Feature: Run tasks from web interface
+  In order to run scheduled tasks immediately
+  As an admin
+  I need to be able to run a task from the web interface
+
+  Scenario: Run a task
+    Given I log in as "admin"
+    When I navigate to "Scheduled tasks" node in "Site administration > Server"
+    Then I should see "Never" in the "Log table cleanup" "table_row"
+
+    And I click on "Run now" "text" in the "Log table cleanup" "table_row"
+    And I should see "Are you sure you want to run this task"
+    And I press "Run now"
+
+    And I should see "Log table cleanup" in the "h2" "css_element"
+    And I should see "Scheduled task complete: Log table cleanup"
+
+    And I follow "Back to scheduled tasks"
+    And I should not see "Never" in the "Log table cleanup" "table_row"
+
+  Scenario: Cancel running a task
+    Given I log in as "admin"
+    When I navigate to "Scheduled tasks" node in "Site administration > Server"
+    And I click on "Run now" "text" in the "Log table cleanup" "table_row"
+    And I press "Cancel"
+    # Confirm we're back on the scheduled tasks page by looking for the table.
+    Then "Log table cleanup" "table_row" should exist
+
+  Scenario: Cannot run a task when the option is disabled
+    Given the following config values are set as admin:
+      | enablerunnow | 0 | tool_task |
+    When I log in as "admin"
+    And I navigate to "Scheduled tasks" node in "Site administration > Server"
+    Then I should not see "Run now"
index fa8bb71..164a767 100644 (file)
@@ -64,45 +64,7 @@ function cron_run() {
     // Run all scheduled tasks.
     while (!\core\task\manager::static_caches_cleared_since($timenow) &&
            $task = \core\task\manager::get_next_scheduled_task($timenow)) {
-        $fullname = $task->get_name() . ' (' . get_class($task) . ')';
-        mtrace('Execute scheduled task: ' . $fullname);
-        cron_trace_time_and_memory();
-        $predbqueries = null;
-        $predbqueries = $DB->perf_get_queries();
-        $pretime      = microtime(1);
-        try {
-            get_mailer('buffer');
-            $task->execute();
-            if ($DB->is_transaction_started()) {
-                throw new coding_exception("Task left transaction open");
-            }
-            if (isset($predbqueries)) {
-                mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
-                mtrace("... used " . (microtime(1) - $pretime) . " seconds");
-            }
-            mtrace('Scheduled task complete: ' . $fullname);
-            \core\task\manager::scheduled_task_complete($task);
-        } catch (Exception $e) {
-            if ($DB && $DB->is_transaction_started()) {
-                error_log('Database transaction aborted automatically in ' . get_class($task));
-                $DB->force_transaction_rollback();
-            }
-            if (isset($predbqueries)) {
-                mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
-                mtrace("... used " . (microtime(1) - $pretime) . " seconds");
-            }
-            mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
-            if ($CFG->debugdeveloper) {
-                 if (!empty($e->debuginfo)) {
-                    mtrace("Debug info:");
-                    mtrace($e->debuginfo);
-                }
-                mtrace("Backtrace:");
-                mtrace(format_backtrace($e->getTrace(), true));
-            }
-            \core\task\manager::scheduled_task_failed($task);
-        }
-        get_mailer('close');
+        cron_run_inner_scheduled_task($task);
         unset($task);
     }
 
@@ -158,6 +120,140 @@ function cron_run() {
     mtrace("Execution took ".$difftime." seconds");
 }
 
+/**
+ * Shared code that handles running of a single scheduled task within the cron.
+ *
+ * Not intended for calling directly outside of this library!
+ *
+ * @param \core\task\task_base $task
+ */
+function cron_run_inner_scheduled_task(\core\task\task_base $task) {
+    global $CFG, $DB;
+
+    $fullname = $task->get_name() . ' (' . get_class($task) . ')';
+    mtrace('Execute scheduled task: ' . $fullname);
+    cron_trace_time_and_memory();
+    $predbqueries = null;
+    $predbqueries = $DB->perf_get_queries();
+    $pretime = microtime(1);
+    try {
+        get_mailer('buffer');
+        $task->execute();
+        if ($DB->is_transaction_started()) {
+            throw new coding_exception("Task left transaction open");
+        }
+        if (isset($predbqueries)) {
+            mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
+            mtrace("... used " . (microtime(1) - $pretime) . " seconds");
+        }
+        mtrace('Scheduled task complete: ' . $fullname);
+        \core\task\manager::scheduled_task_complete($task);
+    } catch (Exception $e) {
+        if ($DB && $DB->is_transaction_started()) {
+            error_log('Database transaction aborted automatically in ' . get_class($task));
+            $DB->force_transaction_rollback();
+        }
+        if (isset($predbqueries)) {
+            mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
+            mtrace("... used " . (microtime(1) - $pretime) . " seconds");
+        }
+        mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
+        if ($CFG->debugdeveloper) {
+            if (!empty($e->debuginfo)) {
+                mtrace("Debug info:");
+                mtrace($e->debuginfo);
+            }
+            mtrace("Backtrace:");
+            mtrace(format_backtrace($e->getTrace(), true));
+        }
+        \core\task\manager::scheduled_task_failed($task);
+    }
+    get_mailer('close');
+}
+
+/**
+ * Runs a single cron task. This function assumes it is displaying output in pseudo-CLI mode.
+ *
+ * The function will fail if the task is disabled.
+ *
+ * Warning: Because this function closes the browser session, it may not be safe to continue
+ * with other processing (other than displaying the rest of the page) after using this function!
+ *
+ * @param \core\task\scheduled_task $task Task to run
+ * @return bool True if cron run successful
+ */
+function cron_run_single_task(\core\task\scheduled_task $task) {
+    global $CFG, $DB, $USER;
+
+    if (CLI_MAINTENANCE) {
+        echo "CLI maintenance mode active, cron execution suspended.\n";
+        return false;
+    }
+
+    if (moodle_needs_upgrading()) {
+        echo "Moodle upgrade pending, cron execution suspended.\n";
+        return false;
+    }
+
+    // Check task and component is not disabled.
+    $taskname = get_class($task);
+    if ($task->get_disabled()) {
+        echo "Task is disabled ($taskname).\n";
+        return false;
+    }
+    $component = $task->get_component();
+    if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) {
+        if (!$plugininfo->is_enabled() && !$task->get_run_if_component_disabled()) {
+            echo "Component is not enabled ($component).\n";
+            return false;
+        }
+    }
+
+    // Enable debugging features as per config settings.
+    if (!empty($CFG->showcronsql)) {
+        $DB->set_debug(true);
+    }
+    if (!empty($CFG->showcrondebugging)) {
+        set_debugging(DEBUG_DEVELOPER, true);
+    }
+
+    // Increase time and memory limits.
+    core_php_time_limit::raise();
+    raise_memory_limit(MEMORY_EXTRA);
+
+    // Switch to admin account for cron tasks, but close the session so we don't send this stuff
+    // to the browser.
+    session_write_close();
+    $realuser = clone($USER);
+    cron_setup_user(null, null, true);
+
+    // Get lock for cron task.
+    $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
+    if (!$cronlock = $cronlockfactory->get_lock('core_cron', 1)) {
+        echo "Unable to get cron lock.\n";
+        return false;
+    }
+    if (!$lock = $cronlockfactory->get_lock($taskname, 1)) {
+        $cronlock->release();
+        echo "Unable to get task lock for $taskname.\n";
+        return false;
+    }
+    $task->set_lock($lock);
+    if (!$task->is_blocking()) {
+        $cronlock->release();
+    } else {
+        $task->set_cron_lock($cronlock);
+    }
+
+    // Run actual tasks.
+    cron_run_inner_scheduled_task($task);
+
+    // Go back to real user account.
+    cron_setup_user($realuser, null, true);
+
+    return true;
+}
+
 /**
  * Output some standard information during cron runs. Specifically current time
  * and memory usage. This method also does gc_collect_cycles() (before displaying
index 8bdfa86..9dce516 100644 (file)
@@ -8735,8 +8735,13 @@ function address_in_subnet($addr, $subnetstr) {
  *                      This ensures any messages have time to display before redirect
  */
 function mtrace($string, $eol="\n", $sleep=0) {
+    global $CFG;
 
-    if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
+    if (isset($CFG->mtrace_wrapper) && function_exists($CFG->mtrace_wrapper)) {
+        $fn = $CFG->mtrace_wrapper;
+        $fn($string, $eol);
+        return;
+    } else if (defined('STDOUT') && !PHPUNIT_TEST && !defined('BEHAT_TEST')) {
         fwrite(STDOUT, $string.$eol);
     } else {
         echo $string . $eol;
index 03fc861..3777434 100644 (file)
@@ -178,12 +178,13 @@ function get_moodle_cookie() {
  * @param stdClass $user full user object, null means default cron user (admin),
  *                 value 'reset' means reset internal static caches.
  * @param stdClass $course full course record, null means $SITE
+ * @param bool $leavepagealone If specified, stops it messing with global page object
  * @return void
  */
-function cron_setup_user($user = NULL, $course = NULL) {
+function cron_setup_user($user = null, $course = null, $leavepagealone = false) {
     global $CFG, $SITE, $PAGE;
 
-    if (!CLI_SCRIPT) {
+    if (!CLI_SCRIPT && !$leavepagealone) {
         throw new coding_exception('Function cron_setup_user() cannot be used in normal requests!');
     }
 
@@ -224,11 +225,13 @@ function cron_setup_user($user = NULL, $course = NULL) {
 
     // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
     // Temporary hack so that cron does not give fatal errors.
-    $PAGE = new moodle_page();
-    if ($course) {
-        $PAGE->set_course($course);
-    } else {
-        $PAGE->set_course($SITE);
+    if (!$leavepagealone) {
+        $PAGE = new moodle_page();
+        if ($course) {
+            $PAGE->set_course($course);
+        } else {
+            $PAGE->set_course($SITE);
+        }
     }
 
     // TODO: it should be possible to improve perf by caching some limited number of users here ;-)