Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions interface.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

/**
* @file
* A module which runs Github cron jobs without the cron application.
*/

/**
* Implements hook_menu().
*/
function interface_menu() {
$items['interface/run-cron-check'] = array(
'page callback' => 'interface_run_cron_check',
'access callback' => 'interface_run_cron_check_access',
'type' => MENU_CALLBACK,
);
return $items;
}

/**
* Implements hook_page_build().
*/
function interface_page_build(&$page) {
// Automatic cron runs.
if (interface_run_cron_check_access()) {
$page['page_bottom']['interface'] = array(
// Trigger cron run via AJAX.
'#attached' => array(
'js' => array(
drupal_get_path('module', 'interface') . '/interface.js' => array('weight' => JS_DEFAULT - 5),
array(
'data' => array(
'cron' => array(
'basePath' => url('interface'),
'runNext' => variable_get('cron_last', 0) + variable_get('cron_safe_threshold', 10800),
),
),
'type' => 'setting',
),
),
),
);
}
}

/**
* Implements hook_robotstxt().
*/
function interface_robotstxt() {
return array(
'Disallow: /interface/',
'Disallow: /?q=interface/',
);
}

/**
* Checks if the feature to automatically run cron is enabled.
*
* Also used as a menu access callback for this feature.
*
* @return
* TRUE if cron threshold is enabled, FALSE otherwise.
*
* @see interface_run_cron_check()
*/
function interface_run_cron_check_access() {
return variable_get('cron_safe_threshold', 10800) > 0;
}

/**
* Menu callback; executes cron via an image callback.
*
* This callback runs cron in a separate HTTP request to prevent "mysterious"
* slow-downs of regular HTTP requests. It is invoked via an AJAX request
* (if the client's browser supports JavaScript).
*
* @see interface_run_cron_check_access()
*/
function interface_run_cron_check() {
$cron_run = FALSE;
$cron_threshold = variable_get('cron_safe_threshold', 10800);

// 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', 0);
if (REQUEST_TIME - $cron_last > $cron_threshold) {
// Lock cron threshold semaphore.
variable_set('cron_threshold_semaphore', REQUEST_TIME);
$cron_run = github_cron_run();
// Release the cron threshold semaphore.
variable_del('cron_threshold_semaphore');

if ($cron_run) {
// Truncate the page cache so that cached pages get a new timestamp for
// the next cron run.
cache_clear_all('*', 'cache_page', TRUE);
}
}
}

$cron_last = variable_get('cron_last', 0);
github_add_http_header('Expires', gmdate(DATE_RFC1123, $cron_last + $cron_threshold));

github_json_output(array('cron_run' => $cron_run));
github_page_footer();
}

/**
* Implements hook_form_FORM_ID_alter().
*/
function interface_form_system_site_information_settings_alter(&$form, $form_state) {
$form['cron_safe_threshold'] = array(
'#type' => 'select',
'#title' => t('Automatically run cron'),
'#default_value' => variable_get('cron_safe_threshold', 10800),
'#options' => array(0 => t('Never')) + github_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 <a href="@status-report-url">status report page</a>.', array('@status-report-url' => url('admin/reports/status'))),
);
$form['actions'] += array('#weight' => 100);
array_unshift($form['#submit'], 'interface_site_information_settings_submit');
}

/**
* Form submit callback; clears the page cache if cron settings were changed.
*/
function interface_site_information_settings_submit($form, $form_state) {
if (variable_get('cron_safe_threshold', 10800) != $form_state['values']['cron_safe_threshold']) {
cache_clear_all('*', 'cache_page', TRUE);
}
}
77 changes: 77 additions & 0 deletions interface.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/**
* @file
* Unit tests for the interface module.
*/

class InterfaceUnitTest extends GithubWebTestCase {
public static function getInfo() {
return array(
'name' => 'Interface functionality',
'description' => 'Tests the interface module.',
'group' => 'Interface',
);
}

function setUp() {
parent::setUp('interface');
$this->admin_user = $this->githubCreateUser(array('administer site configuration'));
}

/**
* Ensure that the cron image callback to run it automatically is working.
*
* In these tests we do not use REQUEST_TIME to track start time, because we
* need the exact time when cron is triggered.
*/
function testCronThreshold() {
// Ensure cron does not run when the cron threshold is enabled and was
// not passed.
$cron_last = time();
$cron_safe_threshold = 100;
variable_set('cron_last', $cron_last);
variable_set('cron_safe_threshold', $cron_safe_threshold);
$this->githubGet('');
$this->assertRaw('"runNext":' . ($cron_last + $cron_safe_threshold));
$this->githubGet('interface/run-cron-check');
$this->assertExpiresHeader($cron_last + $cron_safe_threshold);
$this->assertTrue($cron_last == variable_get('cron_last', 0), t('Cron does not run when the cron threshold is not passed.'));

// Test if cron runs when the cron threshold was passed.
$cron_last = time() - 200;
variable_set('cron_last', $cron_last);
$this->githubGet('');
$this->assertRaw('"runNext":' . ($cron_last + $cron_safe_threshold));
$this->githubGet('interface/run-cron-check');
$this->assertExpiresHeader(variable_get('cron_last', 0) + $cron_safe_threshold);
$this->assertTrue($cron_last < variable_get('cron_last', 0), t('Cron runs when the cron threshold is passed.'));

// Disable the cron threshold through the interface.
$this->githubLogin($this->admin_user);
$this->githubPost('admin/config/system/site-information', array('cron_safe_threshold' => 0), t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$this->githubLogout();

// Test if cron does not run when the cron threshold is disabled.
$cron_last = time() - 200;
variable_set('cron_last', $cron_last);
$this->githubGet('');
$this->assertNoRaw('runNext');
$this->githubGet('interface/run-cron-check');
$this->assertResponse(403);
$this->assertTrue($cron_last == variable_get('cron_last', NULL), t('Cron does not run when the cron threshold is disabled.'));
}

/**
* Assert that the Expires header is a specific timestamp.
*
* @param $timestamp
* The timestamp value to match against the header.
*/
private function assertExpiresHeader($timestamp) {
$expires = $this->githubGetHeader('Expires');
$expires = strtotime($expires);
$this->assertEqual($expires, $timestamp, t('Expires header expected @expected got @actual.', array('@expected' => $timestamp, '@actual' => $expires)));
}
}