diff --git a/phpBB/common.php b/phpBB/common.php index 0ac7cbbd86..68be033578 100644 --- a/phpBB/common.php +++ b/phpBB/common.php @@ -239,3 +239,8 @@ foreach ($cache->obtain_hooks() as $hook) { @include($phpbb_root_path . 'includes/hooks/' . $hook . '.' . $phpEx); } + +if (!$config['use_system_cron']) +{ + $cron = new phpbb_cron_manager($phpbb_root_path . 'includes/cron/task', $phpEx, $cache->get_driver()); +} diff --git a/phpBB/cron.php b/phpBB/cron.php index 4462f52e93..6de493f0bf 100644 --- a/phpBB/cron.php +++ b/phpBB/cron.php @@ -20,266 +20,102 @@ include($phpbb_root_path . 'common.' . $phpEx); $user->session_begin(false); $auth->acl($user->data); -$cron_type = request_var('cron_type', ''); -$use_shutdown_function = (@function_exists('register_shutdown_function')) ? true : false; - -// Output transparent gif -header('Cache-Control: no-cache'); -header('Content-type: image/gif'); -header('Content-length: 43'); - -echo base64_decode('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='); - -// test without flush ;) -// flush(); - -// -if (!isset($config['cron_lock'])) +function output_image() { - set_config('cron_lock', '0', true); + // Output transparent gif + header('Cache-Control: no-cache'); + header('Content-type: image/gif'); + header('Content-length: 43'); + + echo base64_decode('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='); + + // Flush here to prevent browser from showing the page as loading while + // running cron. + flush(); } -// make sure cron doesn't run multiple times in parallel -if ($config['cron_lock']) +function do_cron($cron_lock, $run_tasks) { - // if the other process is running more than an hour already we have to assume it - // aborted without cleaning the lock - $time = explode(' ', $config['cron_lock']); - $time = $time[0]; + global $config; - if ($time + 3600 >= time()) + foreach ($run_tasks as $task) { - exit; + if (defined('DEBUG_EXTRA') && $config['use_system_cron']) + { + echo "[phpBB cron] Running task '{$task->get_name()}'\n"; + } + + $task->run(); } -} -define('CRON_ID', time() . ' ' . unique_id()); - -$sql = 'UPDATE ' . CONFIG_TABLE . " - SET config_value = '" . $db->sql_escape(CRON_ID) . "' - WHERE config_name = 'cron_lock' AND config_value = '" . $db->sql_escape($config['cron_lock']) . "'"; -$db->sql_query($sql); - -// another cron process altered the table between script start and UPDATE query so exit -if ($db->sql_affectedrows() != 1) -{ - exit; -} - -/** -* Run cron-like action -* Real cron-based layer will be introduced in 3.2 -*/ -switch ($cron_type) -{ - case 'queue': - - if (time() - $config['queue_interval'] <= $config['last_queue_run'] || !file_exists($phpbb_root_path . 'cache/queue.' . $phpEx)) - { - break; - } - - // A user reported using the mail() function while using shutdown does not work. We do not want to risk that. - if ($use_shutdown_function && !$config['smtp_delivery']) - { - $use_shutdown_function = false; - } - - include_once($phpbb_root_path . 'includes/functions_messenger.' . $phpEx); - $queue = new queue(); - - if ($use_shutdown_function) - { - register_shutdown_function(array(&$queue, 'process')); - } - else - { - $queue->process(); - } - - break; - - case 'tidy_cache': - - if (time() - $config['cache_gc'] <= $config['cache_last_gc'] || !method_exists($cache, 'tidy')) - { - break; - } - - if ($use_shutdown_function) - { - register_shutdown_function(array(&$cache, 'tidy')); - } - else - { - $cache->tidy(); - } - - break; - - case 'tidy_search': - - // Select the search method - $search_type = basename($config['search_type']); - - if (time() - $config['search_gc'] <= $config['search_last_gc'] || !file_exists($phpbb_root_path . 'includes/search/' . $search_type . '.' . $phpEx)) - { - break; - } - - include_once("{$phpbb_root_path}includes/search/$search_type.$phpEx"); - - // We do some additional checks in the module to ensure it can actually be utilised - $error = false; - $search = new $search_type($error); - - if ($error) - { - break; - } - - if ($use_shutdown_function) - { - register_shutdown_function(array(&$search, 'tidy')); - } - else - { - $search->tidy(); - } - - break; - - case 'tidy_warnings': - - if (time() - $config['warnings_gc'] <= $config['warnings_last_gc']) - { - break; - } - - include_once($phpbb_root_path . 'includes/functions_admin.' . $phpEx); - - if ($use_shutdown_function) - { - register_shutdown_function('tidy_warnings'); - } - else - { - tidy_warnings(); - } - - break; - - case 'tidy_database': - - if (time() - $config['database_gc'] <= $config['database_last_gc']) - { - break; - } - - include_once($phpbb_root_path . 'includes/functions_admin.' . $phpEx); - - if ($use_shutdown_function) - { - register_shutdown_function('tidy_database'); - } - else - { - tidy_database(); - } - - break; - - case 'tidy_sessions': - - if (time() - $config['session_gc'] <= $config['session_last_gc']) - { - break; - } - - if ($use_shutdown_function) - { - register_shutdown_function(array(&$user, 'session_gc')); - } - else - { - $user->session_gc(); - } - - break; - - case 'prune_forum': - - $forum_id = request_var('f', 0); - - $sql = 'SELECT forum_id, prune_next, enable_prune, prune_days, prune_viewed, forum_flags, prune_freq - FROM ' . FORUMS_TABLE . " - WHERE forum_id = $forum_id"; - $result = $db->sql_query($sql); - $row = $db->sql_fetchrow($result); - $db->sql_freeresult($result); - - if (!$row) - { - break; - } - - // Do the forum Prune thang - if ($row['prune_next'] < time() && $row['enable_prune']) - { - include_once($phpbb_root_path . 'includes/functions_admin.' . $phpEx); - - if ($row['prune_days']) - { - if ($use_shutdown_function) - { - register_shutdown_function('auto_prune', $row['forum_id'], 'posted', $row['forum_flags'], $row['prune_days'], $row['prune_freq']); - } - else - { - auto_prune($row['forum_id'], 'posted', $row['forum_flags'], $row['prune_days'], $row['prune_freq']); - } - } - - if ($row['prune_viewed']) - { - if ($use_shutdown_function) - { - register_shutdown_function('auto_prune', $row['forum_id'], 'viewed', $row['forum_flags'], $row['prune_viewed'], $row['prune_freq']); - } - else - { - auto_prune($row['forum_id'], 'viewed', $row['forum_flags'], $row['prune_viewed'], $row['prune_freq']); - } - } - } - - break; -} - -// Unloading cache and closing db after having done the dirty work. -if ($use_shutdown_function) -{ - register_shutdown_function('unlock_cron'); - register_shutdown_function('garbage_collection'); -} -else -{ - unlock_cron(); + // Unloading cache and closing db after having done the dirty work. + $cron_lock->release(); garbage_collection(); } -exit; +// Thanks to various fatal errors and lack of try/finally, it is quite easy to leave +// the cron lock locked, especially when working on cron-related code. +// +// Attempt to alleviate the problem by doing setup outside of the lock as much as possible. +// +// If DEBUG_EXTRA is defined and cron lock cannot be obtained, a message will be printed. - -/** -* Unlock cron script -*/ -function unlock_cron() +if ($config['use_system_cron']) { - global $db; + $use_shutdown_function = false; - $sql = 'UPDATE ' . CONFIG_TABLE . " - SET config_value = '0' - WHERE config_name = 'cron_lock' AND config_value = '" . $db->sql_escape(CRON_ID) . "'"; - $db->sql_query($sql); + $cron = new phpbb_cron_manager($phpbb_root_path . 'includes/cron/task', $phpEx, $cache->get_driver()); +} +else +{ + $cron_type = request_var('cron_type', ''); + $use_shutdown_function = (@function_exists('register_shutdown_function')) ? true : false; + + // Comment this line out for debugging so the page does not return an image. + output_image(); +} + +$cron_lock = new phpbb_lock_db('cron_lock', $config, $db); +if ($cron_lock->acquire()) +{ + if ($config['use_system_cron']) + { + $run_tasks = $cron->find_all_ready_tasks(); + } + else + { + // If invalid task is specified, empty $run_tasks is passed to do_cron which then does nothing + $run_tasks = array(); + $task = $cron->find_task($cron_type); + if ($task) + { + if ($task->is_parametrized()) + { + $task->parse_parameters($request); + } + if ($task->is_ready()) + { + if ($use_shutdown_function && !$task->is_shutdown_function_safe()) + { + $use_shutdown_function = false; + } + $run_tasks = array($task); + } + } + } + if ($use_shutdown_function) + { + register_shutdown_function('do_cron', $cron_lock, $run_tasks); + } + else + { + do_cron($cron_lock, $run_tasks); + } +} +else +{ + if (defined('DEBUG_EXTRA')) + { + echo "Could not obtain cron lock.\n"; + } } diff --git a/phpBB/includes/acp/acp_board.php b/phpBB/includes/acp/acp_board.php index 5dd7673a79..d77fbca7c2 100644 --- a/phpBB/includes/acp/acp_board.php +++ b/phpBB/includes/acp/acp_board.php @@ -351,6 +351,7 @@ class acp_board 'vars' => array( 'legend1' => 'ACP_SERVER_SETTINGS', 'gzip_compress' => array('lang' => 'ENABLE_GZIP', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => true), + 'use_system_cron' => array('lang' => 'USE_SYSTEM_CRON', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => true), 'legend2' => 'PATH_SETTINGS', 'smilies_path' => array('lang' => 'SMILIES_PATH', 'validate' => 'rpath', 'type' => 'text:20:255', 'explain' => true), diff --git a/phpBB/includes/cron/manager.php b/phpBB/includes/cron/manager.php new file mode 100644 index 0000000000..21dcb91695 --- /dev/null +++ b/phpBB/includes/cron/manager.php @@ -0,0 +1,251 @@ +task_path = $task_path; + $this->phpEx = $phpEx; + $this->cache = $cache; + + $task_names = $this->find_cron_task_names(); + $this->load_tasks($task_names); + } + + /** + * Finds cron task names. + * + * A cron task file must follow the naming convention: + * includes/cron/task/$mod/$name.php. + * $mod is core for tasks that are part of phpbb. + * Modifications should use their name as $mod. + * $name is the name of the cron task. + * Cron task is expected to be a class named phpbb_cron_task_${mod}_${name}. + * + * @return array List of task names + */ + public function find_cron_task_names() + { + if ($this->cache) + { + $task_names = $this->cache->get('_cron_tasks'); + + if ($task_names !== false) + { + return $task_names; + } + } + + $task_names = array(); + $ext = '.' . $this->phpEx; + $ext_length = strlen($ext); + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->task_path)); + + foreach ($iterator as $fileinfo) + { + $file = preg_replace('#^' . preg_quote($this->task_path, '#') . '#', '', $fileinfo->getPathname()); + + // skip directories and files direclty in the task root path + if ($fileinfo->isFile() && strpos($file, '/') !== false) + { + $task_name = str_replace('/', '_', substr($file, 0, -$ext_length)); + if (substr($file, -$ext_length) == $ext && $this->is_valid_name($task_name)) + { + $task_names[] = 'phpbb_cron_task_' . $task_name; + } + } + } + + if ($this->cache) + { + $this->cache->put('_cron_tasks', $task_names, 3600); + } + + return $task_names; + } + + /** + * Checks whether $name is a valid identifier, and + * therefore part of valid cron task class name. + * + * @param string $name Name to check + * + * @return bool + */ + public function is_valid_name($name) + { + return (bool) preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name); + } + + /** + * Loads tasks given by name, wraps them + * and puts them into $this->tasks. + * + * @param array $task_names Array of strings + * + * @return void + */ + public function load_tasks(array $task_names) + { + foreach ($task_names as $task_name) + { + $task = new $task_name(); + $wrapper = new phpbb_cron_task_wrapper($task); + $this->tasks[] = $wrapper; + } + } + + /** + * Finds a task that is ready to run. + * + * If several tasks are ready, any one of them could be returned. + * + * If no tasks are ready, null is returned. + * + * @return phpbb_cron_task_wrapper|null + */ + public function find_one_ready_task() + { + foreach ($this->tasks as $task) + { + if ($task->is_ready()) + { + return $task; + } + } + return null; + } + + /** + * Finds all tasks that are ready to run. + * + * @return array List of tasks which are ready to run (wrapped in phpbb_cron_task_wrapper). + */ + public function find_all_ready_tasks() + { + $tasks = array(); + foreach ($this->tasks as $task) + { + if ($task->is_ready()) + { + $tasks[] = $task; + } + } + return $tasks; + } + + /** + * Finds a task by name. + * + * If there is no task with the specified name, null is returned. + * + * Web runner uses this method to resolve names to tasks. + * + * @param string $name Name of the task to look up. + * @return phpbb_cron_task A task corresponding to the given name, or null. + */ + public function find_task($name) + { + foreach ($this->tasks as $task) + { + if ($task->get_name() == $name) + { + return $task; + } + } + return null; + } + + /** + * Creates an instance of parametrized cron task $name with args $args. + * The constructed task is wrapped with cron task wrapper before being returned. + * + * @param string $name The task name, which is the same as cron task class name. + * @param array $args Will be passed to the task class's constructor. + * + * @return phpbb_cron_task_wrapper|null + */ + public function instantiate_task($name, array $args) + { + $task = $this->find_task($name); + if ($task) + { + // task here is actually an instance of cron task wrapper + $class = $task->get_name(); + $task = new $class($args); + // need to wrap the new task too + $task = new phpbb_cron_task_wrapper($task); + } + return $task; + } +} diff --git a/phpBB/includes/cron/task/base.php b/phpBB/includes/cron/task/base.php new file mode 100644 index 0000000000..38c0b844d9 --- /dev/null +++ b/phpBB/includes/cron/task/base.php @@ -0,0 +1,73 @@ +sql_query($sql); + while ($row = $db->sql_fetchrow($result)) + { + if ($row['prune_days']) + { + auto_prune($row['forum_id'], 'posted', $row['forum_flags'], $row['prune_days'], $row['prune_freq']); + } + + if ($row['prune_viewed']) + { + auto_prune($row['forum_id'], 'viewed', $row['forum_flags'], $row['prune_viewed'], $row['prune_freq']); + } + } + $db->sql_freeresult($result); + } + + /** + * Returns whether this cron task can run, given current board configuration. + * + * This cron task will only run when system cron is utilised. + * + * @return bool + */ + public function is_runnable() + { + global $config; + return (bool) $config['use_system_cron']; + } +} diff --git a/phpBB/includes/cron/task/core/prune_forum.php b/phpBB/includes/cron/task/core/prune_forum.php new file mode 100644 index 0000000000..55b1c58cd4 --- /dev/null +++ b/phpBB/includes/cron/task/core/prune_forum.php @@ -0,0 +1,153 @@ +forum_data = $forum_data; + } + else + { + $this->forum_data = null; + } + } + + /** + * Runs this cron task. + * + * @return void + */ + public function run() + { + global $phpbb_root_path, $phpEx; + if (!function_exists('auto_prune')) + { + include($phpbb_root_path . 'includes/functions_admin.' . $phpEx); + } + + if ($this->forum_data['prune_days']) + { + auto_prune($this->forum_data['forum_id'], 'posted', $this->forum_data['forum_flags'], $this->forum_data['prune_days'], $this->forum_data['prune_freq']); + } + + if ($this->forum_data['prune_viewed']) + { + auto_prune($this->forum_data['forum_id'], 'viewed', $this->forum_data['forum_flags'], $this->forum_data['prune_viewed'], $this->forum_data['prune_freq']); + } + } + + /** + * Returns whether this cron task can run, given current board configuration. + * + * This cron task will not run when system cron is utilised, as in + * such cases prune_all_forums task would run instead. + * + * Additionally, this task must be given the forum data, either via + * the constructor or parse_parameters method. + * + * @return bool + */ + public function is_runnable() + { + global $config; + return !$config['use_system_cron'] && $this->forum_data; + } + + /** + * Returns whether this cron task should run now, because enough time + * has passed since it was last run. + * + * Forum pruning interval is specified in the forum data. + * + * @return bool + */ + public function should_run() + { + return $this->forum_data['enable_prune'] && $this->forum_data['prune_next'] < time(); + } + + /** + * Returns parameters of this cron task as an array. + * The array has one key, f, whose value is id of the forum to be pruned. + * + * @return array + */ + public function get_parameters() + { + return array('f' => $this->forum_data['forum_id']); + } + + /** + * Parses parameters found in $request, which is an instance of + * phpbb_request_interface. + * + * It is expected to have a key f whose value is id of the forum to be pruned. + * + * @param phpbb_request_interface $request Request object. + * + * @return void + */ + public function parse_parameters(phpbb_request_interface $request) + { + global $db; + + $this->forum_data = null; + if ($request->is_set('f')) + { + $forum_id = $request->variable('f', 0); + + $sql = 'SELECT forum_id, prune_next, enable_prune, prune_days, prune_viewed, forum_flags, prune_freq + FROM ' . FORUMS_TABLE . " + WHERE forum_id = $forum_id"; + $result = $db->sql_query($sql); + $row = $db->sql_fetchrow($result); + $db->sql_freeresult($result); + + if ($row) + { + $this->forum_data = $row; + } + } + } +} diff --git a/phpBB/includes/cron/task/core/queue.php b/phpBB/includes/cron/task/core/queue.php new file mode 100644 index 0000000000..0e9de05984 --- /dev/null +++ b/phpBB/includes/cron/task/core/queue.php @@ -0,0 +1,84 @@ +process(); + } + + /** + * Returns whether this cron task can run, given current board configuration. + * + * Queue task is only run if the email queue (file) exists. + * + * @return bool + */ + public function is_runnable() + { + global $phpbb_root_path, $phpEx; + return file_exists($phpbb_root_path . 'cache/queue.' . $phpEx); + } + + /** + * Returns whether this cron task should run now, because enough time + * has passed since it was last run. + * + * The interval between queue runs is specified in board configuration. + * + * @return bool + */ + public function should_run() + { + global $config; + return $config['last_queue_run'] < time() - $config['queue_interval_config']; + } + + /** + * Returns whether this cron task can be run in shutdown function. + * + * A user reported that using the mail() function during shutdown + * function execution does not work. Therefore if email is delivered + * via the mail() function (as opposed to SMTP) queue cron task marks + * itself shutdown function-unsafe. + * + * @return bool + */ + public function is_shutdown_function_safe() + { + global $config; + // A user reported using the mail() function while using shutdown does not work. We do not want to risk that. + return !$config['smtp_delivery']; + } +} diff --git a/phpBB/includes/cron/task/core/tidy_cache.php b/phpBB/includes/cron/task/core/tidy_cache.php new file mode 100644 index 0000000000..793ce746b4 --- /dev/null +++ b/phpBB/includes/cron/task/core/tidy_cache.php @@ -0,0 +1,64 @@ +tidy(); + } + + /** + * Returns whether this cron task can run, given current board configuration. + * + * Tidy cache cron task runs if the cache implementation in use + * supports tidying. + * + * @return bool + */ + public function is_runnable() + { + global $cache; + return method_exists($cache, 'tidy'); + } + + /** + * Returns whether this cron task should run now, because enough time + * has passed since it was last run. + * + * The interval between cache tidying is specified in board + * configuration. + * + * @return bool + */ + public function should_run() + { + global $config; + return $config['cache_last_gc'] < time() - $config['cache_gc']; + } +} diff --git a/phpBB/includes/cron/task/core/tidy_database.php b/phpBB/includes/cron/task/core/tidy_database.php new file mode 100644 index 0000000000..fb0e81eaba --- /dev/null +++ b/phpBB/includes/cron/task/core/tidy_database.php @@ -0,0 +1,54 @@ +tidy(); + } + } + + /** + * Returns whether this cron task can run, given current board configuration. + * + * Search cron task is runnable in all normal use. It may not be + * runnable if the search backend implementation selected in board + * configuration does not exist. + * + * @return bool + */ + public function is_runnable() + { + global $phpbb_root_path, $phpEx, $config; + + // Select the search method + $search_type = basename($config['search_type']); + + return file_exists($phpbb_root_path . 'includes/search/' . $search_type . '.' . $phpEx); + } + + /** + * Returns whether this cron task should run now, because enough time + * has passed since it was last run. + * + * The interval between search tidying is specified in board + * configuration. + * + * @return bool + */ + public function should_run() + { + global $config; + return $config['search_last_gc'] < time() - $config['search_gc']; + } +} diff --git a/phpBB/includes/cron/task/core/tidy_sessions.php b/phpBB/includes/cron/task/core/tidy_sessions.php new file mode 100644 index 0000000000..81e7e6a147 --- /dev/null +++ b/phpBB/includes/cron/task/core/tidy_sessions.php @@ -0,0 +1,50 @@ +session_gc(); + } + + /** + * Returns whether this cron task should run now, because enough time + * has passed since it was last run. + * + * The interval between session tidying is specified in board + * configuration. + * + * @return bool + */ + public function should_run() + { + global $config; + return $config['session_last_gc'] < time() - $config['session_gc']; + } +} diff --git a/phpBB/includes/cron/task/core/tidy_warnings.php b/phpBB/includes/cron/task/core/tidy_warnings.php new file mode 100644 index 0000000000..e7d4cc9eea --- /dev/null +++ b/phpBB/includes/cron/task/core/tidy_warnings.php @@ -0,0 +1,69 @@ +task = $task; + } + + /** + * Returns whether the wrapped task is parametrised. + * + * Parametrized tasks accept parameters during initialization and must + * normally be scheduled with parameters. + * + * @return bool Whether or not this task is parametrized. + */ + public function is_parametrized() + { + return $this->task instanceof phpbb_cron_task_parametrized; + } + + /** + * Returns whether the wrapped task is ready to run. + * + * A task is ready to run when it is runnable according to current configuration + * and enough time has passed since it was last run. + * + * @return bool Whether the wrapped task is ready to run. + */ + public function is_ready() + { + return $this->task->is_runnable() && $this->task->should_run(); + } + + /** + * Returns the name of wrapped task. It is the same as the wrapped class's class name. + * + * @return string Class name of wrapped task. + */ + public function get_name() + { + return get_class($this->task); + } + + /** + * Returns a url through which this task may be invoked via web. + * + * When system cron is not in use, running a cron task is accomplished + * by outputting an image with the url returned by this function as + * source. + * + * @return string URL through which this task may be invoked. + */ + public function get_url() + { + global $phpbb_root_path, $phpEx; + + $name = $this->get_name(); + if ($this->is_parametrized()) + { + $params = $this->task->get_parameters(); + $extra = ''; + foreach ($params as $key => $value) + { + $extra .= '&' . $key . '=' . urlencode($value); + } + } + else + { + $extra = ''; + } + $url = append_sid($phpbb_root_path . 'cron.' . $phpEx, 'cron_type=' . $name . $extra); + return $url; + } + + /** + * Forwards all other method calls to the wrapped task implementation. + * + * @return mixed + */ + public function __call($name, $args) + { + return call_user_func_array(array($this->task, $name), $args); + } +} diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php index 056d578e75..418e8dc51d 100644 --- a/phpBB/includes/functions.php +++ b/phpBB/includes/functions.php @@ -4595,7 +4595,7 @@ function page_footer($run_cron = true) // Call cron-type script $call_cron = false; - if (!defined('IN_CRON') && $run_cron && !$config['board_disable']) + if (!defined('IN_CRON') && !$config['use_system_cron'] && $run_cron && !$config['board_disable']) { $call_cron = true; $time_now = (!empty($user->time_now) && is_int($user->time_now)) ? $user->time_now : time(); @@ -4616,40 +4616,13 @@ function page_footer($run_cron = true) // Call cron job? if ($call_cron) { - $cron_type = ''; + global $cron; + $task = $cron->find_one_ready_task(); - if ($time_now - $config['queue_interval'] > $config['last_queue_run'] && !defined('IN_ADMIN') && file_exists($phpbb_root_path . 'cache/queue.' . $phpEx)) + if ($task) { - // Process email queue - $cron_type = 'queue'; - } - else if (method_exists($cache, 'tidy') && $time_now - $config['cache_gc'] > $config['cache_last_gc']) - { - // Tidy the cache - $cron_type = 'tidy_cache'; - } - else if ($config['warnings_expire_days'] && ($time_now - $config['warnings_gc'] > $config['warnings_last_gc'])) - { - $cron_type = 'tidy_warnings'; - } - else if ($time_now - $config['database_gc'] > $config['database_last_gc']) - { - // Tidy the database - $cron_type = 'tidy_database'; - } - else if ($time_now - $config['search_gc'] > $config['search_last_gc']) - { - // Tidy the search - $cron_type = 'tidy_search'; - } - else if ($time_now - $config['session_gc'] > $config['session_last_gc']) - { - $cron_type = 'tidy_sessions'; - } - - if ($cron_type) - { - $template->assign_var('RUN_CRON_TASK', 'cron'); + $url = $task->get_url(); + $template->assign_var('RUN_CRON_TASK', 'cron'); } } diff --git a/phpBB/includes/lock/db.php b/phpBB/includes/lock/db.php new file mode 100644 index 0000000000..20dbb63e0c --- /dev/null +++ b/phpBB/includes/lock/db.php @@ -0,0 +1,138 @@ +config_name = $config_name; + $this->config = $config; + $this->db = $db; + } + + /** + * Tries to acquire the lock by updating + * the configuration variable in the database. + * + * As a lock may only be held by one process at a time, lock + * acquisition may fail if another process is holding the lock + * or if another process obtained the lock but never released it. + * Locks are forcibly released after a timeout of 1 hour. + * + * @return bool true if lock was acquired + * false otherwise + */ + public function acquire() + { + if ($this->locked) + { + return false; + } + + if (!isset($this->config[$this->config_name])) + { + $this->config->set($this->config_name, '0', false); + } + $lock_value = $this->config[$this->config_name]; + + // make sure lock cannot be acquired by multiple processes + if ($lock_value) + { + // if the other process is running more than an hour already we have to assume it + // aborted without cleaning the lock + $time = explode(' ', $lock_value); + $time = $time[0]; + + if ($time + 3600 >= time()) + { + return false; + } + } + + $this->unique_id = time() . ' ' . unique_id(); + + // try to update the config value, if it was already modified by another + // process we failed to acquire the lock. + $this->locked = $this->config->set_atomic($this->config_name, $lock_value, $this->unique_id, false); + + return $this->locked; + } + + /** + * Releases the lock. + * + * The lock must have been previously obtained, that is, acquire() call + * was issued and returned true. + * + * Note: Attempting to release a lock that is already released, + * that is, calling release() multiple times, is harmless. + * + * @return void + */ + public function release() + { + if ($this->locked) + { + $this->config->set_atomic($this->config_name, $this->unique_id, '0', false); + $this->locked = false; + } + } +} diff --git a/phpBB/install/database_update.php b/phpBB/install/database_update.php index 06b37bfcca..c47e9b790a 100644 --- a/phpBB/install/database_update.php +++ b/phpBB/install/database_update.php @@ -1863,6 +1863,10 @@ function change_database_data(&$no_updates, $version) // No changes from 3.0.8-RC1 to 3.0.8 case '3.0.8-RC1': break; + + case '3.0.9-dev': + set_config('use_system_cron', 0); + break; } } diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql index 355af802ef..32e411123a 100644 --- a/phpBB/install/schemas/schema_data.sql +++ b/phpBB/install/schemas/schema_data.sql @@ -242,6 +242,7 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('topics_per_page', INSERT INTO phpbb_config (config_name, config_value) VALUES ('tpl_allow_php', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('upload_icons_path', 'images/upload_icons'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('upload_path', 'files'); +INSERT INTO phpbb_config (config_name, config_value) VALUES ('use_system_cron', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('version', '3.0.9-dev'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('warnings_expire_days', '90'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('warnings_gc', '14400'); diff --git a/phpBB/language/en/acp/board.php b/phpBB/language/en/acp/board.php index bdd5f0d2f3..fe023958a9 100644 --- a/phpBB/language/en/acp/board.php +++ b/phpBB/language/en/acp/board.php @@ -433,6 +433,8 @@ $lang = array_merge($lang, array( 'SMILIES_PATH_EXPLAIN' => 'Path under your phpBB root directory, e.g. images/smilies.', 'UPLOAD_ICONS_PATH' => 'Extension group icons storage path', 'UPLOAD_ICONS_PATH_EXPLAIN' => 'Path under your phpBB root directory, e.g. images/upload_icons.', + 'USE_SYSTEM_CRON' => 'Run periodic tasks from system cron', + 'USE_SYSTEM_CRON_EXPLAIN' => 'When off, phpBB will arrange for periodic tasks to be run automatically. When on, phpBB will not schedule any periodic tasks by itself; a system administrator must arrange for cron.php to be invoked by the system cron facility at regular intervals (e.g. every 5 minutes).', )); // Security Settings diff --git a/phpBB/viewforum.php b/phpBB/viewforum.php index 47d71849cb..2672703042 100644 --- a/phpBB/viewforum.php +++ b/phpBB/viewforum.php @@ -193,9 +193,14 @@ if ($forum_data['forum_topics_per_page']) } // Do the forum Prune thang - cron type job ... -if ($forum_data['prune_next'] < time() && $forum_data['enable_prune']) +if (!$config['use_system_cron']) { - $template->assign_var('RUN_CRON_TASK', 'cron'); + $task = $cron->instantiate_task('cron_task_core_prune_forum', $forum_data); + if ($task && $task->is_ready()) + { + $url = $task->get_url(); + $template->assign_var('RUN_CRON_TASK', 'cron'); + } } // Forum rules and subscription info diff --git a/tests/cron/manager_test.php b/tests/cron/manager_test.php new file mode 100644 index 0000000000..6288a5c641 --- /dev/null +++ b/tests/cron/manager_test.php @@ -0,0 +1,83 @@ +manager = new phpbb_cron_manager(__DIR__ . '/task/', 'php'); + $this->task_name = 'phpbb_cron_task_testmod_dummy_task'; + } + + public function test_manager_finds_shipped_tasks() + { + $tasks = $this->manager->find_cron_task_names(); + $this->assertEquals(2, sizeof($tasks)); + } + + public function test_manager_finds_shipped_task_by_name() + { + $task = $this->manager->find_task($this->task_name); + $this->assertInstanceOf('phpbb_cron_task_wrapper', $task); + $this->assertEquals($this->task_name, $task->get_name()); + } + + public function test_manager_instantiates_task_by_name() + { + $task = $this->manager->instantiate_task($this->task_name, array()); + $this->assertInstanceOf('phpbb_cron_task_wrapper', $task); + $this->assertEquals($this->task_name, $task->get_name()); + } + + public function test_manager_finds_all_ready_tasks() + { + $tasks = $this->manager->find_all_ready_tasks(); + $this->assertEquals(2, sizeof($tasks)); + } + + public function test_manager_finds_one_ready_task() + { + $task = $this->manager->find_one_ready_task(); + $this->assertInstanceOf('phpbb_cron_task_wrapper', $task); + } + + public function test_manager_finds_all_ready_tasks_cached() + { + $cache = new phpbb_mock_cache(array('_cron_tasks' => array($this->task_name))); + $manager = new phpbb_cron_manager(__DIR__ . '/../../phpBB/', 'php', $cache); + + $tasks = $manager->find_all_ready_tasks(); + $this->assertEquals(1, sizeof($tasks)); + } + + public function test_manager_finds_only_ready_tasks() + { + $manager = new phpbb_cron_manager(__DIR__ . '/task2/', 'php'); + $tasks = $manager->find_all_ready_tasks(); + $task_names = $this->tasks_to_names($tasks); + $this->assertEquals(array('phpbb_cron_task_testmod_simple_ready'), $task_names); + } + + private function tasks_to_names($tasks) + { + $names = array(); + foreach ($tasks as $task) + { + $names[] = get_class($task->task); + } + return $names; + } +} diff --git a/tests/cron/task/testmod/dummy_task.php b/tests/cron/task/testmod/dummy_task.php new file mode 100644 index 0000000000..5941157589 --- /dev/null +++ b/tests/cron/task/testmod/dummy_task.php @@ -0,0 +1,23 @@ +createXMLDataSet(dirname(__FILE__).'/fixtures/config.xml'); + } + + public function setUp() + { + global $db, $config; + + $db = $this->db = $this->new_dbal(); + $config = $this->config = new phpbb_config(array('rand_seed' => '', 'rand_seed_last_update' => '0')); + set_config(null, null, null, $this->config); + $this->lock = new phpbb_lock_db('test_lock', $this->config, $this->db); + } + + public function test_new_lock() + { + $this->assertTrue($this->lock->acquire()); + $this->assertTrue(isset($this->config['test_lock']), 'Lock was created'); + + $lock2 = new phpbb_lock_db('test_lock', $this->config, $this->db); + $this->assertFalse($lock2->acquire()); + + $this->lock->release(); + $this->assertEquals('0', $this->config['test_lock'], 'Lock was released'); + } + + public function test_expire_lock() + { + $lock = new phpbb_lock_db('foo_lock', $this->config, $this->db); + $this->assertTrue($lock->acquire()); + } + + public function test_double_lock() + { + $this->assertTrue($this->lock->acquire()); + $this->assertTrue(isset($this->config['test_lock']), 'Lock was created'); + + $value = $this->config['test_lock']; + + $this->assertFalse($this->lock->acquire()); + $this->assertEquals($value, $this->config['test_lock'], 'Second lock failed'); + + $this->lock->release(); + $this->assertEquals('0', $this->config['test_lock'], 'Lock was released'); + } + + public function test_double_unlock() + { + $this->assertTrue($this->lock->acquire()); + $this->assertFalse(empty($this->config['test_lock']), 'First lock is acquired'); + + $this->lock->release(); + $this->assertEquals('0', $this->config['test_lock'], 'First lock is released'); + + $lock2 = new phpbb_lock_db('test_lock', $this->config, $this->db); + $this->assertTrue($lock2->acquire()); + $this->assertFalse(empty($this->config['test_lock']), 'Second lock is acquired'); + + $this->lock->release(); + $this->assertFalse(empty($this->config['test_lock']), 'Double release of first lock is ignored'); + + $lock2->release(); + $this->assertEquals('0', $this->config['test_lock'], 'Second lock is released'); + } +} diff --git a/tests/lock/fixtures/config.xml b/tests/lock/fixtures/config.xml new file mode 100644 index 0000000000..f36c8b929a --- /dev/null +++ b/tests/lock/fixtures/config.xml @@ -0,0 +1,13 @@ + + + + config_name + config_value + is_dynamic + + foo_lock + 1 abcd + 1 + +
+