Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.184 diff -u -p -r1.184 system.admin.inc --- modules/system/system.admin.inc 22 Aug 2009 09:44:56 -0000 1.184 +++ modules/system/system.admin.inc 22 Aug 2009 13:07:33 -0000 @@ -1313,6 +1313,14 @@ function system_site_information_setting '#description' => t('Render all blocks on the default 404 (not found) page. Disabling blocks can help with performance but might leave users with a less functional site.'), '#default_value' => variable_get('site_404_blocks', 0) ); + $form['cron_safe_threshold'] = array( + '#type' => 'select', + '#title' => t('Automatically run cron'), + '#default_value' => variable_get('cron_safe_threshold', DRUPAL_DEFAULT_CRON_THRESHOLD), + '#options' => array(0 => t('Never')) + drupal_map_assoc(array(3600, 10800, 21600, 43200, 86400, 604800), 'format_interval'), + '#description' => t('When enabled, the site will check whether cron has been run in the configured interval and automatically run it upon the next page request. For more information visit the status report page.', array('@status-report-url' => url('admin/reports/status'))), + ); + $form['#validate'][] = 'system_site_information_settings_validate'; return system_settings_form($form); @@ -1331,6 +1339,11 @@ function system_site_information_setting if (!menu_valid_path($item)) { form_set_error('site_frontpage', t("The path '@path' is either invalid or you do not have access to it.", array('@path' => $item['link_path']))); } + // Clear cache when enabling or disabling the cron threshold. + $cron_threshold = variable_get('cron_safe_threshold', DRUPAL_DEFAULT_CRON_THRESHOLD); + if (($cron_threshold > 0 && $form_state['input']['cron_safe_threshold'] == 0) || ($cron_threshold == 0 && $form_state['input']['cron_safe_threshold'] > 0)) { + cache_clear_all(); + } } /** Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.759 diff -u -p -r1.759 system.module --- modules/system/system.module 22 Aug 2009 09:44:56 -0000 1.759 +++ modules/system/system.module 22 Aug 2009 13:07:34 -0000 @@ -42,6 +42,11 @@ define('DRUPAL_MINIMUM_PGSQL', '8.3'); define('DRUPAL_MAXIMUM_TEMP_FILE_AGE', 21600); /** + * Default interval between automatic cron executions in seconds. + */ +define('DRUPAL_DEFAULT_CRON_THRESHOLD', 10800); + +/** * New users will be set to the default time zone at registration. */ define('DRUPAL_USER_TIMEZONE_DEFAULT', 0); @@ -196,6 +201,9 @@ function system_theme() { 'arguments' => array('version' => NULL), ), 'system_compact_link' => array(), + 'system_run_cron_image' => array( + 'arguments' => array('image_path' => NULL), + ), )); } @@ -496,6 +504,12 @@ function system_menu() { 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); + $items['system/run-cron-image'] = array( + 'title' => 'Execute cron', + 'page callback' => 'system_run_cron_image', + 'access callback' => 'system_run_cron_image_access', + 'type' => MENU_CALLBACK, + ); $items['admin'] = array( 'title' => 'Administer', 'access arguments' => array('access administration pages'), @@ -2968,3 +2982,88 @@ function system_retrieve_file($url, $des return $local; } + +/** + * Implement hook_page_alter(). + */ +function system_page_alter(&$page) { + // We always add triggers to run cron automatically when this feature + // is enabled; it avoids troubles with cached pages. + if (system_run_cron_image_access()) { + $page['page_bottom']['run_cron'] = array( + // GET the image callback with XMLHttpRequest, it will work only + // when Javascript is available. + '#attached_js' => array( + '(function($){ $.get(' . drupal_to_js(url('system/run-cron-image')) . '); })(jQuery);' => array('type' => 'inline', 'scope' => 'header'), + ), + // Image callback as HTML code for a graceful degradation when Javascript + // is not available. + '#markup' => theme('system_run_cron_image', 'system/run-cron-image'), + ); + } +} + +/** + * Menu callback; executes cron via an image callback. + * + * A transparent 1x1 image is rendered to activate a special path that will + * run cron. An image is used to avoid disturbing the user's primary HTTP + * request. During this new request, cron will run if it has not run for an + * amount of time defined in the administrative settings. + */ +function system_run_cron_image() { + drupal_page_is_cacheable(FALSE); + + // Output a transparent 1x1 image to the browser. + drupal_set_header('Content-Type', 'image/gif'); + echo "\x47\x49\x46\x38\x39\x61\x1\x0\x1\x0\x80\xff\x0\xc0\xc0\xc0\x0\x0\x0\x21\xf9\x4\x1\x0\x0\x0\x0\x2c\x0\x0\x0\x0\x1\x0\x1\x0\x0\x2\x2\x44\x1\x0\x3b"; + + // Cron threshold semaphore is used to avoid errors every time the image + // callback is displayed when a previous cron is still running. + $threshold_semaphore = variable_get('cron_threshold_semaphore', FALSE); + if ($threshold_semaphore) { + if (REQUEST_TIME - $threshold_semaphore > 3600) { + // Either cron has been running for more than an hour or the semaphore + // was not reset due to a database error. + watchdog('cron', 'Cron has been running for more than an hour and is most likely stuck.', array(), WATCHDOG_ERROR); + + // Release the cron threshold semaphore. + variable_del('cron_threshold_semaphore'); + } + } + else { + // Run cron automatically if it has never run or threshold was crossed. + $cron_last = variable_get('cron_last', NULL); + $cron_threshold = variable_get('cron_safe_threshold', DRUPAL_DEFAULT_CRON_THRESHOLD); + if (!isset($cron_last) || (REQUEST_TIME - $cron_last > $cron_threshold)) { + // Lock cron threshold semaphore. + variable_set('cron_threshold_semaphore', REQUEST_TIME); + drupal_cron_run(); + // Release the cron threshold semaphore. + variable_del('cron_threshold_semaphore'); + } + } + + exit; +} + +/** + * Checks if the feature to run cron automatically is enabled. + * + * Also used as a menu access callback for this feature. + * + * @return + * TRUE if cron threshold is enabled, FALSE otherwise. + */ +function system_run_cron_image_access() { + return variable_get('cron_safe_threshold', DRUPAL_DEFAULT_CRON_THRESHOLD) > 0; +} + +/** + * Display image used to run cron automatically. + * + * @ingroup themeable + */ +function theme_system_run_cron_image($image_path) { + return ''; +} Index: modules/system/system.test =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.test,v retrieving revision 1.68 diff -u -p -r1.68 system.test --- modules/system/system.test 22 Aug 2009 09:44:56 -0000 1.68 +++ modules/system/system.test 22 Aug 2009 13:07:34 -0000 @@ -391,6 +391,50 @@ class CronRunTestCase extends DrupalWebT } /** + * Follow every image paths in the previously retrieved content. + */ + function drupalGetAllImages() { + foreach ($this->xpath('//img') as $image) { + $this->drupalGet($this->getAbsoluteUrl($image['src'])); + } + } + + /** + * Ensure that the cron image callback to run it automatically is working. + */ + function testCronThreshold() { + // In the following tests we do not use REQUEST_TIME to track start time, + // because we need the exact time when cron is triggered. + + // Ensure cron does not run when the cron threshold is enabled and was + // not passed. + $start_cron_last = time(); + variable_set('cron_last', $start_cron_last); + variable_set('cron_safe_threshold', 10); + $this->drupalGet(''); + // Follow every image path on the page. + $this->drupalGetAllImages(); + $this->assertTrue($start_cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is not passed.')); + + // Test if cron runs when the cron threshold was passed. + $start_cron_last = time() - 15; + variable_set('cron_last', $start_cron_last); + $this->drupalGet(''); + // Follow every image path on the page. + $this->drupalGetAllImages(); + $this->assertTrue(variable_get('cron_last', NULL) > $start_cron_last, t('Cron runs when the cron threshold is passed.')); + + // Test if cron does not run when the cron threshold was is disabled. + $start_cron_last = time() - 15; + variable_set('cron_safe_threshold', 0); + variable_set('cron_last', $start_cron_last); + $this->drupalGet(''); + // Follow every image path on the page. + $this->drupalGetAllImages(); + $this->assertTrue($start_cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is disabled.')); + } + + /** * Ensure that temporary files are removed. */ function testTempFileCleanup() {