From c1f7368e5741136b7b55b6056583f8c124255774 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Sun, 15 Mar 2020 16:54:47 +1100 Subject: [PATCH 1/4] MDL-47271 clilib: Add support for background colors --- lib/clilib.php | 61 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/lib/clilib.php b/lib/clilib.php index c0b908e3921..63b5e4349ca 100644 --- a/lib/clilib.php +++ b/lib/clilib.php @@ -242,38 +242,51 @@ function cli_ansi_format(string $message): string { "" => "\007", // Cursor movement: https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html. - "" => "\033[s", - "" => "\033[u", - "" => "\033[1A", - "" => "\033[1B", - "" => "\033[1C", - "" => "\033[1D", + "" => "\033[s", + "" => "\033[u", + "" => "\033[1A", + "" => "\033[1B", + "" => "\033[1C", + "" => "\033[1D", ]; $colours = [ - 'normal' => '0;0', - 'black' => '0;30', - 'darkGray' => '1;30', - 'blue' => '0;34', - 'lightBlue' => '1;34', - 'green' => '0;32', - 'lightGreen' => '1;32', - 'cyan' => '0;36', - 'lightCyan' => '1;36', - 'red' => '0;31', - 'lightRed' => '1;31', - 'purple' => '0;35', - 'lightPurple' => '1;35', - 'brown' => '0;33', - 'yellow' => '1;33', - 'lightYellow' => '0;93', - 'lightGray' => '0;37', - 'white' => '1;37', + 'normal' => '0;0', + 'black' => '0;30', + 'darkGray' => '1;30', + 'red' => '0;31', + 'lightRed' => '1;31', + 'green' => '0;32', + 'lightGreen' => '1;32', + 'brown' => '0;33', + 'yellow' => '1;33', + 'lightYellow' => '0;93', + 'blue' => '0;34', + 'lightBlue' => '1;34', + 'purple' => '0;35', + 'lightPurple' => '1;35', + 'cyan' => '0;36', + 'lightCyan' => '1;36', + 'lightGray' => '0;37', + 'white' => '1;37', + ]; + $bgcolours = [ + 'black' => '40', + 'red' => '41', + 'green' => '42', + 'yellow' => '43', + 'blue' => '44', + 'magenta' => '45', + 'cyan' => '46', + 'white' => '47', ]; foreach ($colours as $colour => $code) { $replacements[""] = "\033[{$code}m"; } + foreach ($bgcolours as $colour => $code) { + $replacements[""] = "\033[{$code}m"; + } // Windows don't support ANSI code by default, but does if ANSICON is available. $isansicon = getenv('ANSICON'); From 20167daf8943965fa46bdd84a725694ffcad82ab Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Sat, 14 Mar 2020 23:34:49 +1100 Subject: [PATCH 2/4] MDL-47271 report_status: Added System status report --- admin/cli/checks.php | 171 ++++++++++++++++++ lang/en/admin.php | 1 + lang/en/moodle.php | 1 + lib/classes/check/environment/environment.php | 83 +++++++++ .../check/environment/upgradecheck.php | 85 +++++++++ lib/classes/check/manager.php | 28 ++- lib/outputrenderers.php | 35 ++++ report/status/classes/privacy/provider.php | 48 +++++ report/status/db/access.php | 37 ++++ report/status/index.php | 108 +++++++++++ report/status/lang/en/report_status.php | 27 +++ report/status/settings.php | 30 +++ report/status/version.php | 30 +++ 13 files changed, 683 insertions(+), 1 deletion(-) create mode 100644 admin/cli/checks.php create mode 100644 lib/classes/check/environment/environment.php create mode 100644 lib/classes/check/environment/upgradecheck.php create mode 100644 report/status/classes/privacy/provider.php create mode 100644 report/status/db/access.php create mode 100644 report/status/index.php create mode 100644 report/status/lang/en/report_status.php create mode 100644 report/status/settings.php create mode 100644 report/status/version.php diff --git a/admin/cli/checks.php b/admin/cli/checks.php new file mode 100644 index 00000000000..40deb4d5535 --- /dev/null +++ b/admin/cli/checks.php @@ -0,0 +1,171 @@ +. + +/** + * CLI tool for system checks + * + * @package core + * @category check + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../config.php'); +require_once($CFG->libdir.'/clilib.php'); + +use core\check\result; + +list($options, $unrecognized) = cli_get_params([ + 'help' => false, + 'filter' => '', + 'type' => 'status', + 'verbose' => false, +], [ + 'h' => 'help', + 'f' => 'filter', + 'v' => 'verbose', + 't' => 'type', +]); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +$checks = \core\check\manager::get_checks($options['type']); +$types = join(', ', \core\check\manager::TYPES); + +$help = "Run Moodle system checks + +Options: + -h, --help Print out this help + -f, --filter Filter to a subset of checks + -t, --type Which set of checks? Defaults to 'status' + One of $types + -v, --verbose Show details of all checks, not just failed checks + +Example: + + sudo -u www-data php admin/cli/checks.php + sudo -u www-data php admin/cli/checks.php -v + sudo -u www-data php admin/cli/checks.php -v --filter=environment + +"; + +if ($options['help']) { + echo $help; + die(); +} + +$filter = $options['filter']; +if ($filter) { + $checks = array_filter($checks, function($check, $key) use ($filter) { + $ref = $check->get_ref(); + return (strpos($ref, $filter) !== false); + }, 1); +} + +// These shell exit codes and labels align with the NRPE standard. +$exitcodes = [ + result::NA => 0, + result::OK => 0, + result::INFO => 0, + result::UNKNOWN => 3, + result::WARNING => 1, + result::ERROR => 2, + result::CRITICAL => 2, +]; +$exitlabel = [ + result::NA => 'OK', + result::OK => 'OK', + result::INFO => 'OK', + result::UNKNOWN => 'UNKNOWN', + result::WARNING => 'WARNING', + result::ERROR => 'CRITICAL', + result::CRITICAL => 'CRITICAL', +]; + +$format = "% 10s| % -60s\n"; +$spacer = "----------+--------------------------------------------------------------------\n"; +$prefix = ' |'; + +$output = ''; +$header = $exitlabel[result::OK] . ': ' . get_string('checksok', '', $options['type']) . "\n"; +$exitcode = $exitcodes[result::OK]; + +foreach ($checks as $check) { + $ref = $check->get_ref(); + $result = $check->get_result(); + + $status = $result->get_status(); + $checkexitcode = $exitcodes[$status]; + + // Summary is treated as html. + $summary = $result->get_summary(); + $summary = html_to_text($summary, 60, false); + + if ($checkexitcode > $exitcode) { + $exitcode = $checkexitcode; + $header = $exitlabel[$status] . ': ' . $check->get_name() . " (" . $check->get_ref() . ")\n"; + } + + if (empty($messages[$status])) { + $messages[$status] = $result; + } + + $len = strlen(get_string('status' . $status)); + + if ($options['verbose'] || + $status == result::WARNING || + $status == result::CRITICAL || + $status == result::ERROR) { + + $output .= sprintf( + $format, + $OUTPUT->check_result($result), + sprintf('%s (%s)', $check->get_name(), $ref) + ); + + $summary = str_replace("\n", "\n" . $prefix . ' ', $summary); + $output .= sprintf( $format, '', ' ' . $summary); + + if ($options['verbose']) { + $actionlink = $check->get_action_link(); + if ($actionlink) { + $output .= sprintf( $format, '', ' ' . $actionlink->url); + } + $output .= sprintf( $format, '', ''); + } + } +} + +// Print NRPE header. +print $header; + +// Only show the table header if there is anything to show. +if ($output) { + print sprintf($format, + get_string('status'). ' ', + get_string('check') + ) . $spacer; + print $output; +} + +// NRPE shell exit code. +exit($exitcode); + diff --git a/lang/en/admin.php b/lang/en/admin.php index d4a8f676e7f..fefbb20fcd3 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -117,6 +117,7 @@ $string['cannotuninstall'] = '{$a} can not be uninstalled.'; $string['categoryemail'] = 'Email'; $string['cfgwwwrootslashwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It includes a \'/\' character at the end which must be removed.'; $string['cfgwwwrootwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It should match the URL you are using to access this page.'; +$string['checkupgradepending'] = 'Upgrade'; $string['cleanup'] = 'Cleanup'; $string['clianswerno'] = 'n'; $string['cliansweryes'] = 'y'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 7aa080a4e4d..50fb3224281 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1000,6 +1000,7 @@ $string['changepassword'] = 'Change password'; $string['changessaved'] = 'Changes saved'; $string['check'] = 'Check'; $string['checks'] = 'Checks'; +$string['checksok'] = 'All \'{$a}\' checks ok'; $string['checkall'] = 'Check all'; $string['checkingbackup'] = 'Checking backup'; $string['checkingcourse'] = 'Checking course'; diff --git a/lib/classes/check/environment/environment.php b/lib/classes/check/environment/environment.php new file mode 100644 index 00000000000..cf221b72a17 --- /dev/null +++ b/lib/classes/check/environment/environment.php @@ -0,0 +1,83 @@ +. + +/** + * Environment check + * + * @package core + * @category check + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\check\environment; + +defined('MOODLE_INTERNAL') || die(); + +use core\check\check; +use core\check\result; + +/** + * Environment check + * + * @package core + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class environment extends check { + + /** + * Get the short check name + * + * @return string + */ + public function get_name(): string { + return get_string('environment', 'admin'); + } + + /** + * A link to a place to action this + * + * @return action_link|null + */ + public function get_action_link(): ?\action_link { + return new \action_link( + new \moodle_url('/admin/environment.php'), + get_string('environment', 'admin')); + } + + /** + * Return result + * @return result + */ + public function get_result(): result { + global $CFG; + + require_once($CFG->libdir.'/environmentlib.php'); + list($status, $details) = check_moodle_environment($CFG->release, ENV_SELECT_NEWER); + + if ($status) { + $summary = get_string('environmentok', 'admin'); + $status = result::OK; + } else { + $summary = get_string('environmenterrortodo', 'admin'); + $status = result::ERROR; + } + + return new result($status, $summary, ''); + } +} + diff --git a/lib/classes/check/environment/upgradecheck.php b/lib/classes/check/environment/upgradecheck.php new file mode 100644 index 00000000000..706f98723bf --- /dev/null +++ b/lib/classes/check/environment/upgradecheck.php @@ -0,0 +1,85 @@ +. + +/** + * Upgrade check + * + * @package core + * @category check + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\check\environment; + +defined('MOODLE_INTERNAL') || die(); + +use core\check\check; +use core\check\result; + +/** + * Upgrade check + * + * @package core + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class upgradecheck extends check { + + /** + * Get the short check name + * + * @return string + */ + public function get_name(): string { + return get_string('checkupgradepending', 'admin'); + } + + /** + * A link to a place to action this + * + * @return action_link|null + */ + public function get_action_link(): ?\action_link { + return new \action_link( + new \moodle_url('/admin/index.php?cache=1'), + get_string('notifications', 'admin')); + } + + /** + * Return result + * @return result + */ + public function get_result(): result { + global $CFG; + + require("$CFG->dirroot/version.php"); + $newversion = "$release ($version)"; + + if ($version < $CFG->version) { + $status = result::ERROR; + $summary = get_string('downgradedcore', 'error'); + } else if (moodle_needs_upgrading()) { + $status = result::ERROR; + $summary = get_string('cliupgradepending', 'admin'); + } else { + $status = result::OK; + $summary = get_string('cliupgradenoneed', 'core_admin', $newversion); + } + return new result($status, $summary); + } +} + diff --git a/lib/classes/check/manager.php b/lib/classes/check/manager.php index ee0254604e4..cb99ff49200 100644 --- a/lib/classes/check/manager.php +++ b/lib/classes/check/manager.php @@ -40,7 +40,7 @@ class manager { /** * The list of valid check types */ - public const TYPES = ['security']; + public const TYPES = ['status', 'security']; /** * Return all status checks @@ -57,6 +57,32 @@ class manager { return $checks; } + /** + * Return all status checks + * + * @return array of check objects + */ + public static function get_status_checks(): array { + $checks = [ + new environment\environment(), + new environment\upgradecheck(), + ]; + + // Any plugin can add status checks to this report by implementing a callback + // _status_checks() which returns a check object. + $morechecks = get_plugins_with_function('status_checks', 'lib.php'); + foreach ($morechecks as $plugintype => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $result = $pluginfunction(); + foreach ($result as $check) { + $check->set_component($plugintype . '_' . $plugin); + $checks[] = $check; + } + } + } + return $checks; + } + /** * Return all security checks * diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 65a99c4306e..e4d617c1401 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -4775,6 +4775,41 @@ class core_renderer_cli extends core_renderer { return $this->page->heading . "\n"; } + /** + * Renders a Check API result + * + * To aid in CLI consistency this status is NOT translated and the visual + * width is always exactly 10 chars. + * + * @param result $result + * @return string HTML fragment + */ + protected function render_check_result(core\check\result $result) { + $status = $result->get_status(); + + $labels = [ + core\check\result::NA => ' ' . cli_ansi_format('' ) . ' NA ', + core\check\result::OK => ' ' . cli_ansi_format('') . ' OK ', + core\check\result::INFO => ' ' . cli_ansi_format('' ) . ' INFO ', + core\check\result::UNKNOWN => ' ' . cli_ansi_format('' ) . ' UNKNOWN ', + core\check\result::WARNING => ' ' . cli_ansi_format('') . ' WARNING ', + core\check\result::ERROR => ' ' . cli_ansi_format('') . ' ERROR ', + core\check\result::CRITICAL => '' . cli_ansi_format('') . ' CRITICAL ', + ]; + $string = $labels[$status] . cli_ansi_format(''); + return $string; + } + + /** + * Renders a Check API result + * + * @param result $result + * @return string fragment + */ + public function check_result(core\check\result $result) { + return $this->render_check_result($result); + } + /** * Returns a template fragment representing a Heading. * diff --git a/report/status/classes/privacy/provider.php b/report/status/classes/privacy/provider.php new file mode 100644 index 00000000000..a686b907c28 --- /dev/null +++ b/report/status/classes/privacy/provider.php @@ -0,0 +1,48 @@ +. + +/** + * Privacy provider. + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace report_status\privacy; + +defined('MOODLE_INTERNAL') || die; + +/** + * Privacy provider. + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} + diff --git a/report/status/db/access.php b/report/status/db/access.php new file mode 100644 index 00000000000..5e969358478 --- /dev/null +++ b/report/status/db/access.php @@ -0,0 +1,37 @@ +. + +/** + * System status capabilities + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + 'report/status:view' => [ + 'riskbitmask' => RISK_CONFIG, + 'captype' => 'read', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'manager' => CAP_ALLOW + ], + ] +]; diff --git a/report/status/index.php b/report/status/index.php new file mode 100644 index 00000000000..660e01f9b4e --- /dev/null +++ b/report/status/index.php @@ -0,0 +1,108 @@ +. + +/** + * System Status report + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require('../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +use core\check\check; +use core\check\result; + +// Print the header. +admin_externalpage_setup('reportstatus', '', null, '', ['pagelayout' => 'report']); + +// We may need a bit more memory and this may take a long time to process. +raise_memory_limit(MEMORY_EXTRA); +core_php_time_limit::raise(); + +$checks = \core\check\manager::get_checks('status'); + +$detail = optional_param('detail', '', PARAM_TEXT); // Show detailed info about one check only. +if ($detail) { + $checks = array_filter($checks, function($check) use ($detail) { + return $detail == $check->get_ref(); + }); + $checks = array_values($checks); + if (!empty($checks)) { + $PAGE->set_docs_path('report/status/index.php?detail=' . $detail); + $PAGE->navbar->add($checks[0]->get_name()); + } +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('pluginname', 'report_status')); + +$url = "$CFG->wwwroot/report/status/index.php"; + +$table = new html_table(); +$table->data = []; +$table->head = [ + get_string('status'), + get_string('check'), + get_string('summary'), + get_string('action'), +]; +$table->colclasses = [ + 'rightalign status', + 'leftalign check', + 'leftalign summary', + 'leftalign action', +]; +$table->id = 'statusreporttable'; +$table->attributes = ['class' => 'admintable statusreport generaltable']; + +$manager = core_plugin_manager::instance(); + +foreach ($checks as $check) { + $ref = $check->get_ref(); + $result = $check->get_result(); + $component = $check->get_component(); + $actionlink = $check->get_action_link(); + + $link = new \moodle_url('/report/status/index.php', ['detail' => $ref]); + + $row = []; + $row[] = $OUTPUT->result($result); + $row[] = $OUTPUT->action_link($link, $check->get_name()); + + $row[] = $result->get_summary(); + if ($actionlink) { + $row[] = $OUTPUT->render($actionlink); + } else { + $row[] = ''; + } + + $table->data[] = $row; +} +echo html_writer::table($table); + +if ($detail && $result) { + echo $OUTPUT->heading(get_string('description'), 3); + echo $OUTPUT->box($result->get_details(), 'generalbox boxwidthnormal boxaligncenter'); + echo $OUTPUT->continue_button($url); +} + +echo $OUTPUT->footer(); + diff --git a/report/status/lang/en/report_status.php b/report/status/lang/en/report_status.php new file mode 100644 index 00000000000..5251062c224 --- /dev/null +++ b/report/status/lang/en/report_status.php @@ -0,0 +1,27 @@ +. + +/** + * Strings for component 'tool_task', language 'en' + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'System status'; +$string['status:view'] = 'View system status'; +$string['privacy:metadata'] = 'This plugin does not store any personal data.'; diff --git a/report/status/settings.php b/report/status/settings.php new file mode 100644 index 00000000000..0575fe5c967 --- /dev/null +++ b/report/status/settings.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$ADMIN->add('reports', new admin_externalpage('reportstatus', get_string('pluginname', 'report_status'), + "$CFG->wwwroot/report/status/index.php", 'report/status:view')); + +$settings = null; diff --git a/report/status/version.php b/report/status/version.php new file mode 100644 index 00000000000..df6ae86aa02 --- /dev/null +++ b/report/status/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2019111800; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2019111200; // Requires this Moodle version. +$plugin->component = 'report_status'; // Full name of the plugin (used for diagnostics). + From e8e2bd28ec71c13413cea3748f20901cb5306809 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Tue, 17 Mar 2020 02:58:59 +1100 Subject: [PATCH 3/4] MDL-47271 check: Refactor check table into a renderable --- lang/en/moodle.php | 1 + lib/classes/check/table.php | 141 ++++++++++++++++++++++++++++++++++++ report/security/index.php | 82 +++------------------ report/status/index.php | 78 ++------------------ 4 files changed, 159 insertions(+), 143 deletions(-) create mode 100644 lib/classes/check/table.php diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 50fb3224281..89c386bdee6 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -509,6 +509,7 @@ $string['description'] = 'Description'; $string['descriptiona'] = 'Description: {$a}'; $string['deselectall'] = 'Deselect all'; $string['deselectnos'] = 'Deselect all \'No\''; +$string['details'] = 'Details'; $string['detailedless'] = 'Less detailed'; $string['detailedmore'] = 'More detailed'; $string['digitalminor'] = 'Digital minor'; diff --git a/lib/classes/check/table.php b/lib/classes/check/table.php new file mode 100644 index 00000000000..75effc47d9f --- /dev/null +++ b/lib/classes/check/table.php @@ -0,0 +1,141 @@ +. + +/** + * A table of check results + * + * @package core + * @category check + * @copyright 2020 Brendan Heywood + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\check; + +defined('MOODLE_INTERNAL') || die(); + +/** + * A table of check results + * + * @copyright 2020 Brendan Heywood + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class table implements \renderable { + + /** + * @var moodle_url $url + */ + protected $url = ''; + + /** + * @var string $type What type of checks + */ + protected $type = ''; + + /** + * @var check $detail a specific check to focus on + */ + public $detail = ''; + + /** + * @var array $checks shown in this table + */ + public $checks = []; + + /** + * Constructor + * + * @param string $type of check + * @param string $url of report + * @param string $detail check to focus on + */ + public function __construct($type, $url, $detail = '') { + + // We may need a bit more memory and this may take a long time to process. + \raise_memory_limit(MEMORY_EXTRA); + \core_php_time_limit::raise(); + + $this->type = $type; + $this->url = $url; + $this->checks = \core\check\manager::get_checks($type); + + if ($detail) { + $this->checks = array_filter($this->checks, function($check) use ($detail) { + return $detail == $check->get_ref(); + }); + if (!empty($this->checks)) { + $this->detail = reset($this->checks); + } + } + } + + /** + * Render a table of checks + * + * @param renderer $output to use + * @return string html output + */ + public function render($output) { + + $table = new \html_table(); + $table->data = []; + $table->head = [ + get_string('status'), + get_string('check'), + get_string('summary'), + get_string('action'), + ]; + $table->colclasses = [ + 'rightalign status', + 'leftalign check', + 'leftalign summary', + 'leftalign action', + ]; + $table->id = $this->type . 'reporttable'; + $table->attributes = ['class' => 'admintable ' . $this->type . 'report generaltable']; + + foreach ($this->checks as $check) { + $ref = $check->get_ref(); + $result = $check->get_result(); + $component = $check->get_component(); + $actionlink = $check->get_action_link(); + + $link = new \moodle_url($this->url, ['detail' => $ref]); + + $row = []; + $row[] = $output->check_result($result); + $row[] = $output->action_link($link, $check->get_name()); + + $row[] = $result->get_summary(); + if ($actionlink) { + $row[] = $output->render($actionlink); + } else { + $row[] = ''; + } + + $table->data[] = $row; + } + $html = \html_writer::table($table); + + if ($this->detail && $result) { + $html .= $output->heading(get_string('details'), 3); + $html .= $output->box($result->get_details(), 'generalbox boxwidthnormal boxaligncenter'); + $html .= $output->continue_button($this->url); + } + + return $html; + } +} + diff --git a/report/security/index.php b/report/security/index.php index 6a8feae99c4..74d5e59c32a 100644 --- a/report/security/index.php +++ b/report/security/index.php @@ -27,87 +27,23 @@ define('NO_OUTPUT_BUFFERING', true); require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); -use core\check\check; -use core\check\result; - -// Print the header. admin_externalpage_setup('reportsecurity', '', null, '', ['pagelayout' => 'report']); -// We may need a bit more memory and this may take a long time to process. -raise_memory_limit(MEMORY_EXTRA); -core_php_time_limit::raise(); - -$checks = \core\check\manager::get_security_checks(); - $detail = optional_param('detail', '', PARAM_TEXT); // Show detailed info about one check only. -if ($detail) { - $checks = array_filter($checks, function($check) use ($detail) { - return $detail == $check->get_ref(); - }); - $checks = array_values($checks); - if (!empty($checks)) { - $PAGE->set_docs_path('report/security/index.php?detail=' . $detail); - $PAGE->navbar->add($checks[0]->get_name()); - } + +$url = '/report/security/index.php'; +$table = new core\check\table('security', $url, $detail); + +if (!empty($table->detail)) { + $PAGE->set_docs_path($url . '?detail=' . $detail); + $PAGE->navbar->add($table->detail->get_name()); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('pluginname', 'report_security')); - -echo '
' . get_string('timewarning', 'report_security') . '
'; - -$url = "$CFG->wwwroot/report/security/index.php"; - -$PAGE->requires->js_init_code("Y.one('#timewarning').addClass('timewarninghidden')"); -$table = new html_table(); -$table->data = []; -$table->head = [ - get_string('status'), - get_string('check'), - get_string('summary'), - get_string('action'), -]; -$table->colclasses = [ - 'rightalign status', - 'leftalign check', - 'leftalign summary', - 'leftalign action', -]; -$table->id = 'securityreporttable'; -$table->attributes = ['class' => 'admintable securityreport generaltable']; - -$manager = core_plugin_manager::instance(); - -foreach ($checks as $check) { - $ref = $check->get_ref(); - $result = $check->get_result(); - $component = $check->get_component(); - $actionlink = $check->get_action_link(); - - $link = new \moodle_url('/report/security/index.php', ['detail' => $ref]); - - $row = []; - $row[] = $OUTPUT->check_result($result); - $row[] = $OUTPUT->action_link($link, $check->get_name()); - - $row[] = $result->get_summary(); - if ($actionlink) { - $row[] = $OUTPUT->render($actionlink); - } else { - $row[] = ''; - } - - $table->data[] = $row; -} -echo html_writer::table($table); - -if ($detail && $result) { - echo $OUTPUT->heading(get_string('description'), 3); - echo $OUTPUT->box($result->get_details(), 'generalbox boxwidthnormal boxaligncenter'); - echo $OUTPUT->continue_button($url); -} - +echo $table->render($OUTPUT); echo $OUTPUT->footer(); + $event = \report_security\event\report_viewed::create(['context' => context_system::instance()]); $event->trigger(); diff --git a/report/status/index.php b/report/status/index.php index 660e01f9b4e..2d4605ddfdb 100644 --- a/report/status/index.php +++ b/report/status/index.php @@ -27,82 +27,20 @@ define('NO_OUTPUT_BUFFERING', true); require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); -use core\check\check; -use core\check\result; - -// Print the header. admin_externalpage_setup('reportstatus', '', null, '', ['pagelayout' => 'report']); -// We may need a bit more memory and this may take a long time to process. -raise_memory_limit(MEMORY_EXTRA); -core_php_time_limit::raise(); - -$checks = \core\check\manager::get_checks('status'); - $detail = optional_param('detail', '', PARAM_TEXT); // Show detailed info about one check only. -if ($detail) { - $checks = array_filter($checks, function($check) use ($detail) { - return $detail == $check->get_ref(); - }); - $checks = array_values($checks); - if (!empty($checks)) { - $PAGE->set_docs_path('report/status/index.php?detail=' . $detail); - $PAGE->navbar->add($checks[0]->get_name()); - } + +$url = '/report/status/index.php'; +$table = new core\check\table('status', $url, $detail); + +if (!empty($table->detail)) { + $PAGE->set_docs_path($url . '?detail=' . $detail); + $PAGE->navbar->add($table->detail->get_name()); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('pluginname', 'report_status')); - -$url = "$CFG->wwwroot/report/status/index.php"; - -$table = new html_table(); -$table->data = []; -$table->head = [ - get_string('status'), - get_string('check'), - get_string('summary'), - get_string('action'), -]; -$table->colclasses = [ - 'rightalign status', - 'leftalign check', - 'leftalign summary', - 'leftalign action', -]; -$table->id = 'statusreporttable'; -$table->attributes = ['class' => 'admintable statusreport generaltable']; - -$manager = core_plugin_manager::instance(); - -foreach ($checks as $check) { - $ref = $check->get_ref(); - $result = $check->get_result(); - $component = $check->get_component(); - $actionlink = $check->get_action_link(); - - $link = new \moodle_url('/report/status/index.php', ['detail' => $ref]); - - $row = []; - $row[] = $OUTPUT->result($result); - $row[] = $OUTPUT->action_link($link, $check->get_name()); - - $row[] = $result->get_summary(); - if ($actionlink) { - $row[] = $OUTPUT->render($actionlink); - } else { - $row[] = ''; - } - - $table->data[] = $row; -} -echo html_writer::table($table); - -if ($detail && $result) { - echo $OUTPUT->heading(get_string('description'), 3); - echo $OUTPUT->box($result->get_details(), 'generalbox boxwidthnormal boxaligncenter'); - echo $OUTPUT->continue_button($url); -} - +echo $table->render($OUTPUT); echo $OUTPUT->footer(); From 59a44a5bad23d03e8b846e0d730037887369c0eb Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Sat, 14 Mar 2020 23:35:02 +1100 Subject: [PATCH 4/4] MDL-47271 tool_task: Added Task API status checks --- admin/index.php | 5 +- admin/renderer.php | 22 +--- admin/tool/task/classes/check/adhocqueue.php | 93 ++++++++++++++ admin/tool/task/classes/check/cronrunning.php | 118 ++++++++++++++++++ .../tool/task/classes/check/maxfaildelay.php | 96 ++++++++++++++ admin/tool/task/lang/en/tool_task.php | 9 ++ admin/tool/task/lib.php | 39 ++++++ config-dist.php | 6 + lang/en/admin.php | 7 +- 9 files changed, 374 insertions(+), 21 deletions(-) create mode 100644 admin/tool/task/classes/check/adhocqueue.php create mode 100644 admin/tool/task/classes/check/cronrunning.php create mode 100644 admin/tool/task/classes/check/maxfaildelay.php create mode 100644 admin/tool/task/lib.php diff --git a/admin/index.php b/admin/index.php index c391089ea4b..ee3b2e3b85e 100644 --- a/admin/index.php +++ b/admin/index.php @@ -843,8 +843,9 @@ $errorsdisplayed = defined('WARN_DISPLAY_ERRORS_ENABLED'); $lastcron = get_config('tool_task', 'lastcronstart'); $cronoverdue = ($lastcron < time() - 3600 * 24); $lastcroninterval = get_config('tool_task', 'lastcroninterval'); -$expectedfrequency = $CFG->expectedcronfrequency ?? 200; -$croninfrequent = !$cronoverdue && ($lastcroninterval > $expectedfrequency || $lastcron < time() - $expectedfrequency); + +$expectedfrequency = $CFG->expectedcronfrequency ?? MINSECS; +$croninfrequent = !$cronoverdue && ($lastcroninterval > ($expectedfrequency + MINSECS) || $lastcron < time() - $expectedfrequency); $dbproblems = $DB->diagnose(); $maintenancemode = !empty($CFG->maintenance_enabled); diff --git a/admin/renderer.php b/admin/renderer.php index 4ab7754a13b..f9043524f91 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -601,19 +601,9 @@ class core_admin_renderer extends plugin_renderer_base { return ''; } - if (empty($CFG->cronclionly)) { - $url = new moodle_url('/admin/cron.php'); - if (!empty($CFG->cronremotepassword)) { - $url = new moodle_url('/admin/cron.php', array('password' => $CFG->cronremotepassword)); - } - - return $this->warning(get_string('cronwarning', 'admin', $url->out()) . ' ' . - $this->help_icon('cron', 'admin')); - } - - // $CFG->cronclionly is not empty: cron can run only from CLI. - return $this->warning(get_string('cronwarningcli', 'admin') . ' ' . - $this->help_icon('cron', 'admin')); + $check = new \tool_task\check\cronrunning(); + $result = $check->get_result(); + return $this->warning($result->get_summary() . ' ' . $this->help_icon('cron', 'admin')); } /** @@ -629,9 +619,9 @@ class core_admin_renderer extends plugin_renderer_base { return ''; } - $expectedfrequency = $CFG->expectedcronfrequency ?? 200; - return $this->warning(get_string('croninfrequent', 'admin', $expectedfrequency) . ' ' . - $this->help_icon('cron', 'admin')); + $check = new \tool_task\check\cronrunning(); + $result = $check->get_result(); + return $this->warning($result->get_summary() . ' ' . $this->help_icon('cron', 'admin')); } /** diff --git a/admin/tool/task/classes/check/adhocqueue.php b/admin/tool/task/classes/check/adhocqueue.php new file mode 100644 index 00000000000..85f98b8f502 --- /dev/null +++ b/admin/tool/task/classes/check/adhocqueue.php @@ -0,0 +1,93 @@ +. + +/** + * Ad hoc queue checks + * + * @package tool_task + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_task\check; + +defined('MOODLE_INTERNAL') || die(); + +use core\check\check; +use core\check\result; + +/** + * Ad hoc queue checks + * + * @package tool_task + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class adhocqueue extends check { + + /** + * Constructor + */ + public function __construct() { + global $CFG; + $this->id = 'adhocqueue'; + $this->name = get_string('checkadhocqueue', 'tool_task'); + } + + /** + * Return result + * @return result + */ + public function get_result() : result { + global $DB, $CFG; + + $stats = $DB->get_record_sql(' + SELECT count(*) cnt, + MAX(? - nextruntime) age + FROM {task_adhoc}', [time()]); + + $status = result::OK; + $summary = get_string('adhocempty', 'tool_task'); + $details = ''; + + if ($stats->cnt > 0) { + // A large queue size by itself is not an issue, only when tasks + // are not being processed in a timely fashion is it an issue. + $status = result::INFO; + $summary = get_string('adhocqueuesize', 'tool_task', $stats->cnt); + } + + $max = $CFG->adhoctaskagewarn ?? 10 * MINSECS; + if ($stats->age > $max) { + $status = result::WARNING; + $summary = get_string('adhocqueueold', 'tool_task', [ + 'age' => format_time($stats->age), + 'max' => format_time($max), + ]); + } + + $max = $CFG->adhoctaskageerror ?? 4 * HOURSECS; + if ($stats->age > $max) { + $status = result::ERROR; + $summary = get_string('adhocqueueold', 'tool_task', [ + 'age' => format_time($stats->age), + 'max' => format_time($max), + ]); + } + + return new result($status, $summary, $details); + } +} diff --git a/admin/tool/task/classes/check/cronrunning.php b/admin/tool/task/classes/check/cronrunning.php new file mode 100644 index 00000000000..46aa6908231 --- /dev/null +++ b/admin/tool/task/classes/check/cronrunning.php @@ -0,0 +1,118 @@ +. + +/** + * Cron running check + * + * @package tool_task + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_task\check; + +defined('MOODLE_INTERNAL') || die(); + +use core\check\check; +use core\check\result; +/** + * Cron running check + * + * @package tool_task + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cronrunning extends check { + + /** + * Constructor + */ + public function __construct() { + global $CFG; + $this->id = 'cronrunning'; + $this->name = get_string('checkcronrunning', 'tool_task'); + if (empty($CFG->cronclionly)) { + $this->actionlink = new \action_link( + new \moodle_url('/admin/cron.php'), + get_string('cron', 'admin')); + } + } + + /** + * Return result + * @return result + */ + public function get_result() : result { + global $CFG; + + // Eventually this should replace cron_overdue_warning and + // cron_infrequent_warning. + $lastcron = get_config('tool_task', 'lastcronstart'); + $expectedfrequency = $CFG->expectedcronfrequency ?? MINSECS; + + $delta = time() - $lastcron; + + $lastcroninterval = get_config('tool_task', 'lastcroninterval'); + + $formatdelta = format_time($delta); + $formatexpected = format_time($expectedfrequency); + $formatinterval = format_time($lastcroninterval); + + $details = format_time($delta); + + if ($delta > $expectedfrequency + MINSECS) { + $status = result::WARNING; + + if ($delta > DAYSECS) { + $status = result::CRITICAL; + } + + if (empty($lastcron)) { + $summary = get_string('cronwarningnever', 'admin', [ + 'expected' => $formatexpected, + ]); + } else if (empty($CFG->cronclionly)) { + $url = new \moodle_url('/admin/cron.php'); + $summary = get_string('cronwarning', 'admin', [ + 'url' => $url->out(), + 'actual' => $formatdelta, + 'expected' => $formatexpected, + ]); + } else { + $summary = get_string('cronwarningcli', 'admin', [ + 'actual' => $formatdelta, + 'expected' => $formatexpected, + ]); + } + return new result($status, $summary, $details); + } + + if ($lastcroninterval > $expectedfrequency) { + $status = result::WARNING; + $summary = get_string('croninfrequent', 'admin', [ + 'actual' => $formatinterval, + 'expected' => $formatexpected, + ]); + return new result($status, $summary, $details); + } + + $status = result::OK; + $summary = get_string('cronok', 'tool_task'); + + return new result($status, $summary, $details); + } +} + diff --git a/admin/tool/task/classes/check/maxfaildelay.php b/admin/tool/task/classes/check/maxfaildelay.php new file mode 100644 index 00000000000..019aa75edf1 --- /dev/null +++ b/admin/tool/task/classes/check/maxfaildelay.php @@ -0,0 +1,96 @@ +. + +/** + * Task fail delay check + * + * @package tool_task + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_task\check; + +defined('MOODLE_INTERNAL') || die(); + +use core\check\check; +use core\check\result; + +/** + * Task fail delay check + * + * @package tool_task + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class maxfaildelay extends check { + + /** + * Constructor + */ + public function __construct() { + global $CFG; + $this->id = 'cronfaildelay'; + $this->name = get_string('checkmaxfaildelay', 'tool_task'); + $this->actionlink = new \action_link( + new \moodle_url('/admin/tool/task/scheduledtasks.php'), + get_string('scheduledtasks', 'tool_task')); + } + + /** + * Return result + * @return result + */ + public function get_result() : result { + global $CFG; + + $status = result::OK; + $summary = get_string('tasknofailures', 'tool_task'); + $details = ''; + $failures = 0; + $maxdelay = 0; + + $tasks = \core\task\manager::get_all_scheduled_tasks(); + foreach ($tasks as $task) { + if ($task->get_disabled()) { + continue; + } + $faildelay = $task->get_fail_delay(); + if ($faildelay > $maxdelay) { + $maxdelay = $faildelay; + } + if ($faildelay > 0) { + $failures++; + $details .= get_string('faildelay', 'tool_task') . ': ' . format_time($faildelay); + $details .= ' - ' . $task->get_name() . ' (' .get_class($task) . ")
"; + } + } + + if ($failures > 0) { + // Intermittent failures are not yet a warning. + $status = result::INFO; + $summary = get_string('taskfailures', 'tool_task', $failures); + } + if ($maxdelay > 5 * MINSECS) { + $status = result::WARNING; + } + if ($maxdelay > 4 * HOURSECS) { + $status = result::ERROR; + } + + return new result($status, $summary, $details); + } +} diff --git a/admin/tool/task/lang/en/tool_task.php b/admin/tool/task/lang/en/tool_task.php index e48348a0e58..43c9540d404 100644 --- a/admin/tool/task/lang/en/tool_task.php +++ b/admin/tool/task/lang/en/tool_task.php @@ -23,12 +23,19 @@ */ $string['asap'] = 'ASAP'; +$string['adhocempty'] = 'Adhoc task queue is empty'; +$string['adhocqueuesize'] = 'Adhoc task queue has {$a} tasks'; +$string['adhocqueueold'] = 'Oldest task is {$a->age} which is more than {$a->max}'; $string['backtoscheduledtasks'] = 'Back to scheduled tasks'; $string['blocking'] = 'Blocking'; $string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.'; +$string['checkadhocqueue'] = 'Adhoc task queue'; +$string['checkcronrunning'] = 'Cron running'; +$string['checkmaxfaildelay'] = 'Tasks max fail delay'; $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.'; $string['component'] = 'Component'; $string['corecomponent'] = 'Core'; +$string['cronok'] = 'Cron is running frequently'; $string['default'] = 'Default'; $string['defaultx'] = 'Default: {$a}'; $string['disabled'] = 'Disabled'; @@ -50,7 +57,9 @@ $string['runpattern'] = 'Run pattern'; $string['scheduledtasks'] = 'Scheduled tasks'; $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration'; $string['taskdisabled'] = 'Task disabled'; +$string['taskfailures'] = 'There are {$a} task(s) failing'; $string['tasklogs'] = 'Task logs'; +$string['tasknofailures'] = 'There are no tasks failing'; $string['taskscheduleday'] = 'Day'; $string['taskscheduleday_help'] = 'Day of month field for task schedule. The field uses the same format as unix cron. Some examples are: diff --git a/admin/tool/task/lib.php b/admin/tool/task/lib.php new file mode 100644 index 00000000000..ae23257b904 --- /dev/null +++ b/admin/tool/task/lib.php @@ -0,0 +1,39 @@ +. + +/** + * Task API status checks + * + * @package tool_task + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Add cron related service status checks + * + * @return array of check objects + */ +function tool_task_status_checks() : array { + return [ + new \tool_task\check\cronrunning(), + new \tool_task\check\maxfaildelay(), + new \tool_task\check\adhocqueue(), + ]; +} + diff --git a/config-dist.php b/config-dist.php index 5da7105ef30..51f7e55e370 100644 --- a/config-dist.php +++ b/config-dist.php @@ -616,6 +616,12 @@ $CFG->admin = 'admin'; // // $CFG->expectedcronfrequency = 200; // +// Moodle 3.9+ checks how old tasks are in the ad hoc queue and warns at 10 minutes +// and errors at 4 hours. Set these to override these limits: +// +// $CFG->adhoctaskagewarn = 10 * 60; +// $CFG->adhoctaskageerror = 4 * 60 * 60; +// // Session lock warning threshold. Long running pages should release the session using \core\session\manager::write_close(). // Set this threshold to any value greater than 0 to add developer warnings when a page locks the session for too long. // The session should rarely be locked for more than 1 second. The input should be in seconds and may be a float. diff --git a/lang/en/admin.php b/lang/en/admin.php index fefbb20fcd3..5cc4192bdff 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -429,10 +429,11 @@ $string['cron_link'] = 'admin/cron'; $string['cronclionly'] = 'Cron execution via command line only'; $string['cronerrorclionly'] = 'Sorry, internet access to this page has been disabled by the administrator.'; $string['cronerrorpassword'] = 'Sorry, you have not provided a valid password to access this page'; -$string['croninfrequent'] = 'The time between the last two runs of the cron maintenance script was over {$a} seconds. We recommend configuring it to run more frequently.'; +$string['croninfrequent'] = 'There was {$a->actual} between the last two runs of the cron maintenance script and it should run every {$a->expected}. We recommend configuring it to run more frequently.'; $string['cronremotepassword'] = 'Cron password for remote access'; -$string['cronwarning'] = 'The cron.php maintenance script has not been run for at least 24 hours.'; -$string['cronwarningcli'] = 'The cli/cron.php maintenance script has not been run for at least 24 hours.'; +$string['cronwarning'] = 'The admin/cron.php script has not been run for {$a->actual} and should run every {$a->expected}.'; +$string['cronwarningcli'] = 'The admin/cli/cron.php script has not been run for {$a->actual} and should run every {$a->expected}.'; +$string['cronwarningnever'] = 'The admin/cli/cron.php script has never been run and should run every {$a->expected}.'; $string['ctyperequired'] = 'The ctype PHP extension is now required by Moodle, in order to improve site performance and to offer multilingual compatibility.'; $string['curlsecurityallowedport'] = 'cURL allowed ports list'; $string['curlsecurityallowedportsyntax'] = 'List of port numbers that cURL can connect to. Valid entries are integer numbers only. Put each entry on a new line. If left empty, then all ports are allowed. If set, in almost all cases, both 443 and 80 should be specified for cURL to connect to standard HTTPS and HTTP ports.';