*
* Features:
* - crontab-like scheduling configuration of each job.
* - grouping of jobs in channels (parallel lines of execution).
* - you can disable all jobs, an entire channel or a single job via configuration.
* - time statistics of each job and of the whole channel.
* - modules can define extra cron tasks, each one with own default cron-rules
* (site administrators can override them by configuration).
* - administrators can define custom jobs (call to functions with parameters)
* - protection from external cron calling by cron_key or allowed host list.
* - ensure all shutdown hook functions launched by cron jobs are launched inside
* cron protection (ex: search_cron() will launch search_update_totals() in a
* shutdown hook).
*
* TODO Roadmap
* - "set as terminated" on running jobs/contexts
* - confirm page for "reset statistics"
* - better internationalization
* - detect system time "jumps" (if a check occurs BEFORE previous check)
*/
/*******************************************************************************
* DRUPAL HOOKS
******************************************************************************/
function elysia_cron_menu() {
$items['admin/build/cron'] = array(
'title' => 'Cron Settings',
'description' => 'View and manage cron table',
'page callback' => 'elysia_cron_admin_page',
'access arguments' => array('administer elysia_cron'),
);
$items['admin/build/cron/status'] = array(
'type' => MENU_DEFAULT_LOCAL_TASK,
'title' => 'Status',
'weight' => 1,
);
$items['admin/build/cron/reset'] = array(
'type' => MENU_LOCAL_TASK,
'title' => 'Reset statistics',
'page callback' => 'elysia_cron_reset_page',
'access arguments' => array('administer elysia_cron'),
'weight' => 2,
);
$items['admin/build/cron/settings'] = array(
'type' => MENU_LOCAL_TASK,
'title' => 'Settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('elysia_cron_settings_form'),
'access arguments' => array('administer elysia_cron'),
'weight' => 3,
);
$items['admin/build/cron/execute'] = array(
'type' => MENU_CALLBACK,
'page callback' => 'elysia_cron_execute_page',
'access arguments' => array('administer elysia_cron'),
);
return $items;
}
function elysia_cron_perm() {
return array('administer elysia_cron');
}
function elysia_cron_init() {
global $conf;
// I need to set cron_semaphore always to false. That way standard drupal cron is always bypassed
$conf['cron_semaphore'] = false;
}
/**
* Hook cron is invoked only by standard drupal cron.
* It's used to replace drupal cron.
*/
function elysia_cron_cron() {
// If the path is 'admin/*' this is a manual cron run (probably by admin/logs/status)
$manual_run = (arg(0) == 'admin');
$result = elysia_cron_run($manual_run);
// I must check for cron_semaphore and delete it if set (is not always deleted by elysia_cron_run, and this situation is only when called by drupal cron, so a check here is right)
if (variable_get('cron_semaphore', false)) {
global $conf;
_ec_variable_del('cron_semaphore');
$conf['cron_semaphore'] = false;
}
if ($manual_run) {
if ($result) {
drupal_set_message(t('Cron ran successfully'));
}
else {
drupal_set_message(t('Cron run failed, disabled or nothing to do'));
}
drupal_goto('admin/reports/status');
}
exit();
}
/**
* I use help section for admin/build/modules page to check if elysia_cron
* is the module with the smallest weight.
* If it's not i'll set it and print a message
*/
function elysia_cron_help($section, $arg) {
if ($section == 'admin/build/modules') {
$min = db_result(db_query("select min(weight) from {system} where name != '%s'", 'elysia_cron'));
$weight = db_result(db_query("select weight from {system} where name = '%s'", 'elysia_cron'));
if ($min <= $weight) {
drupal_set_message('Elysia cron module is not the module with the smallest weight (and it must be). Updating weight...');
db_query("update {system} set weight = %d where name = '%s'", $min - 1, 'elysia_cron');
}
}
}
/*******************************************************************************
* ELYSIA CRON VERSION UPDATE
******************************************************************************/
/**
* Runtime version update
*/
function elysia_cron_check_version_update() {
$ver = _ec_variable_get('elysia_cron_version', 0);
if (!$ver || $ver < 20090218) {
$unchanged = array(
'elysia_cron_last_context',
'elysia_cron_last_run',
'elysia_cron_disabled',
'elysia_cron_semaphore',
'elysia_cron_key',
'elysia_cron_allowed_hosts',
'elysia_cron_default_rule',
'elysia_cron_script',
'elysia_cron_runtime_replacement',
);
$rs = db_query('select * from {variable} where name like "elysia_cron_%%"');
while ($v = db_fetch_object($rs)) {
if (!in_array($v->name, $unchanged)) {
$vn = false;
if (preg_match('/^elysia_cron_ctx_(.*)_(running|disabled|last_run|last_aborted|abort_count|execution_count|last_execution_time|avg_execution_time|max_execution_time|last_shutdown_time|last_abort_function)/', $v->name, $r)) {
switch ($r[2]) {
case 'running': $vn = 'ecc_'._ec_get_name($r[1]).'_r'; break;
case 'disabled': $vn = 'ecc_'._ec_get_name($r[1]).'_d'; break;
case 'last_run': $vn = 'ecc_'._ec_get_name($r[1]).'_lr'; break;
case 'last_aborted': $vn = 'ecc_'._ec_get_name($r[1]).'_la'; break;
case 'abort_count': $vn = 'ecc_'._ec_get_name($r[1]).'_ac'; break;
case 'execution_count': $vn = 'ecc_'._ec_get_name($r[1]).'_ec'; break;
case 'last_execution_time': $vn = 'ecc_'._ec_get_name($r[1]).'_let'; break;
case 'avg_execution_time': $vn = 'ecc_'._ec_get_name($r[1]).'_aet'; break;
case 'max_execution_time': $vn = 'ecc_'._ec_get_name($r[1]).'_met'; break;
case 'last_shutdown_time': $vn = 'ecc_'._ec_get_name($r[1]).'_lst'; break;
case 'last_abort_function': $vn = 'ecc_'._ec_get_name($r[1]).'_laf'; break;
}
} elseif (preg_match('/^elysia_cron_(.*)_(rule|disabled|context|running|last_run|last_execution_time|execution_count|avg_execution_time|max_execution_time)/', $v->name, $r)) {
switch ($r[2]) {
case 'rule': $vn = 'ec_'._ec_get_name($r[1]).'_rul'; break;
case 'disabled': $vn = 'ec_'._ec_get_name($r[1]).'_d'; break;
case 'context': $vn = 'ec_'._ec_get_name($r[1]).'_c'; break;
case 'running': $vn = 'ec_'._ec_get_name($r[1]).'_r'; break;
case 'last_run': $vn = 'ec_'._ec_get_name($r[1]).'_lr'; break;
case 'last_execution_time': $vn = 'ec_'._ec_get_name($r[1]).'_let'; break;
case 'execution_count': $vn = 'ec_'._ec_get_name($r[1]).'_ec'; break;
case 'avg_execution_time': $vn = 'ec_'._ec_get_name($r[1]).'_aet'; break;
case 'max_execution_time': $vn = 'ec_'._ec_get_name($r[1]).'_met'; break;
}
}
if ($vn)
_ec_variable_set($vn, unserialize($v->value));
else
watchdog('cron', 'Error in update, cant convert %name (value: %value)', array('%name' => $v->name, '%value' => $v->value), WATCHDOG_ERROR);
_ec_variable_del($v->name);
}
}
_ec_variable_set('elysia_cron_version', 20090218);
}
elseif ($ver < 20090920) {
_ec_variable_set('elysia_cron_version', 20090920);
}
}
/*******************************************************************************
* SETTINGS API
******************************************************************************/
function _ec_variable_init() {
global $_ec_variables;
$_ec_variables = array();
$result = db_query("select * from {variable} where name like 'elysia_cron_%%' or name like 'ec_%%' or name like 'ecc_%%' or name = 'cron_key'");
while ($variable = db_fetch_object($result))
$_ec_variables[$variable->name] = unserialize($variable->value);
}
/**
* A substitute for variable_get to avoid cache management
* WARN_UPGRADE
*/
function _ec_variable_get($name, $default) {
//return variable_get($name, $default);
global $_ec_variables;
if (!is_array($_ec_variables))
_ec_variable_init();
// If there is a $GLOBALS['original_conf'] = $conf; at the end of settings.php i consider it.
global $original_conf;
if (isset($original_conf[$name]))
return $original_conf[$name];
if (isset($_ec_variables[$name]))
return $_ec_variables[$name];
return $default;
}
/**
* A substitute for variable_set to avoid cache management
* WARN_UPGRADE
*/
function _ec_variable_set($name, $value) {
global $_ec_variables;
if (!is_array($_ec_variables))
_ec_variable_init();
db_lock_table('variable');
db_query("DELETE FROM {variable} WHERE name = '%s'", $name);
db_query("INSERT INTO {variable} (name, value) VALUES ('%s', '%s')", $name, serialize($value));
db_unlock_tables();
$_ec_variables[$name] = $value;
}
/**
* A substitute for variable_del to avoid cache management
* WARN_UPGRADE
*/
function _ec_variable_del($name) {
global $_ec_variables;
if (!is_array($_ec_variables))
_ec_variable_init();
db_lock_table('variable');
db_query("DELETE FROM {variable} WHERE name = '%s'", $name);
db_unlock_tables();
unset($_ec_variables[$name]);
}
function _ec_get_name($name) {
$maxlen = VERSION >= 6 ? 120 : 40;
if (strlen($name) < $maxlen)
return $name;
$border = ($maxlen - 32) / 2;
return substr($name, 0, $border).md5($name).substr($name, -$border);
}
function elysia_cron_is_context_disabled($context, $default = false) {
return _ec_variable_get('ecc_'._ec_get_name($context).'_d', $default);
}
function elysia_cron_set_context_disabled($context, $v) {
return _ec_variable_set('ecc_'._ec_get_name($context).'_d', $v);
}
function elysia_cron_reset_context_disabled($context) {
return _ec_variable_del('ecc_'._ec_get_name($context).'_d');
}
function elysia_cron_get_context_rule($context, $default = '') {
return _ec_variable_get('ecc_'._ec_get_name($context).'_rul', $default);
}
function elysia_cron_set_context_rule($context, $v) {
return _ec_variable_set('ecc_'._ec_get_name($context).'_rul', $v);
}
function elysia_cron_reset_context_rule($context) {
return _ec_variable_del('ecc_'._ec_get_name($context).'_rul');
}
function elysia_cron_is_context_running($context, $default = false) {
return _ec_variable_get('ecc_'._ec_get_name($context).'_r', $default);
}
function elysia_cron_set_context_running($context, $v) {
return _ec_variable_set('ecc_'._ec_get_name($context).'_r', $v);
}
function elysia_cron_get_context_last_run($context, $default = false) {
return _ec_variable_get('ecc_'._ec_get_name($context).'_lr', $default);
}
function elysia_cron_set_context_last_run($context, $v) {
return _ec_variable_set('ecc_'._ec_get_name($context).'_lr', $v);
}
function elysia_cron_get_context_last_aborted($context, $default = false) {
return _ec_variable_get('ecc_'._ec_get_name($context).'_la', $default);
}
function elysia_cron_set_context_last_aborted($context, $v) {
return _ec_variable_set('ecc_'._ec_get_name($context).'_la', $v);
}
function elysia_cron_get_context_abort_count($context, $default = 0) {
return _ec_variable_get('ecc_'._ec_get_name($context).'_ac', $default);
}
function elysia_cron_set_context_abort_count($context, $v) {
return _ec_variable_set('ecc_'._ec_get_name($context).'_ac', $v);
}
function elysia_cron_get_context_last_abort_function($context, $default = '') {
return _ec_variable_get('ecc_'._ec_get_name($context).'_laf', $default);
}
function elysia_cron_set_context_last_abort_function($context, $job) {
return _ec_variable_set('ecc_'._ec_get_name($context).'_laf', $job);
}
function elysia_cron_get_context_stats($context) {
return array(
'last_run' => _ec_variable_get('ecc_'._ec_get_name($context).'_lr', 0),
'last_execution_time' => _ec_variable_get('ecc_'._ec_get_name($context).'_let', 0),
'execution_count' => _ec_variable_get('ecc_'._ec_get_name($context).'_ec', 0),
'avg_execution_time' => _ec_variable_get('ecc_'._ec_get_name($context).'_aet', 0),
'max_execution_time' => _ec_variable_get('ecc_'._ec_get_name($context).'_met', 0),
'last_shutdown_time' => _ec_variable_get('ecc_'._ec_get_name($context).'_lst', 0),
'last_aborted' => _ec_variable_get('ecc_'._ec_get_name($context).'_la', 0),
'abort_count' => _ec_variable_get('ecc_'._ec_get_name($context).'_ac', 0),
'last_abort_function' => _ec_variable_get('ecc_'._ec_get_name($context).'_laf', ''),
);
}
function elysia_cron_set_context_stats($context, $last_run = -1, $last_execution_time = -1, $execution_count = -1,
$avg_execution_time = -1, $max_execution_time = -1, $last_shutdown_time = -1, $last_aborted = -1,
$abort_count = -1, $last_abort_function = -1) {
if ($last_run != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_lr', $last_run);
if ($last_execution_time != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_let', $last_execution_time);
if ($execution_count != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_ec', $execution_count);
if ($avg_execution_time != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_aet', $avg_execution_time);
if ($max_execution_time != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_met', $max_execution_time);
if ($last_shutdown_time != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_lst', $last_shutdown_time);
if ($last_aborted != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_la', $last_aborted);
if ($abort_count != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_ac', $abort_count);
if ($last_abort_function != -1)
_ec_variable_set('ecc_'._ec_get_name($context).'_laf', $last_abort_function);
}
function elysia_cron_get_job_rule($job, $default = '') {
return _ec_variable_get('ec_'._ec_get_name($job).'_rul', $default);
}
function elysia_cron_set_job_rule($job, $v) {
return _ec_variable_set('ec_'._ec_get_name($job).'_rul', $v);
}
function elysia_cron_reset_job_rule($job) {
return _ec_variable_del('ec_'._ec_get_name($job).'_rul');
}
function elysia_cron_get_job_weight($job, $default = 0) {
return _ec_variable_get('ec_'._ec_get_name($job).'_w', $default);
}
function elysia_cron_set_job_weight($job, $v) {
return _ec_variable_set('ec_'._ec_get_name($job).'_w', $v);
}
function elysia_cron_reset_job_weight($job) {
return _ec_variable_del('ec_'._ec_get_name($job).'_w');
}
function elysia_cron_is_job_disabled($job, $default = false) {
return _ec_variable_get('ec_'._ec_get_name($job).'_d', $default);
}
function elysia_cron_set_job_disabled($job, $v) {
return _ec_variable_set('ec_'._ec_get_name($job).'_d', $v);
}
function elysia_cron_reset_job_disabled($job) {
return _ec_variable_del('ec_'._ec_get_name($job).'_d');
}
function elysia_cron_get_job_context($job, $default = '') {
$c = _ec_variable_get('ec_'._ec_get_name($job).'_c', $default);
return !$c ? $default : $c;
}
function elysia_cron_set_job_context($job, $v) {
return _ec_variable_set('ec_'._ec_get_name($job).'_c', $v);
}
function elysia_cron_reset_job_context($job) {
return _ec_variable_del('ec_'._ec_get_name($job).'_c');
}
function elysia_cron_is_job_running($job, $default = false) {
return _ec_variable_get('ec_'._ec_get_name($job).'_r', $default);
}
function elysia_cron_set_job_running($job, $v) {
return _ec_variable_set('ec_'._ec_get_name($job).'_r', $v);
}
function elysia_cron_get_job_last_run($job, $default = 0) {
return _ec_variable_get('ec_'._ec_get_name($job).'_lr', $default);
}
function elysia_cron_set_job_last_run($job, $v) {
return _ec_variable_set('ec_'._ec_get_name($job).'_lr', $v);
}
function elysia_cron_get_job_stats($job) {
return array(
'last_run' => _ec_variable_get('ec_'._ec_get_name($job).'_lr', 0),
'last_execution_time' => _ec_variable_get('ec_'._ec_get_name($job).'_let', 0),
'execution_count' => _ec_variable_get('ec_'._ec_get_name($job).'_ec', 0),
'avg_execution_time' => _ec_variable_get('ec_'._ec_get_name($job).'_aet', 0),
'max_execution_time' => _ec_variable_get('ec_'._ec_get_name($job).'_met', 0),
);
}
function elysia_cron_set_job_stats($job, $last_run = -1, $last_execution_time = -1, $execution_count = -1,
$avg_execution_time = -1, $max_execution_time= -1) {
if ($last_run != -1)
_ec_variable_set('ec_'._ec_get_name($job).'_lr', $last_run);
if ($last_execution_time != -1)
_ec_variable_set('ec_'._ec_get_name($job).'_let', $last_execution_time);
if ($execution_count != -1)
_ec_variable_set('ec_'._ec_get_name($job).'_ec', $execution_count);
if ($avg_execution_time != -1)
_ec_variable_set('ec_'._ec_get_name($job).'_aet', $avg_execution_time);
if ($max_execution_time != -1)
_ec_variable_set('ec_'._ec_get_name($job).'_met', $max_execution_time);
}
/*******************************************************************************
* INTERFACE
******************************************************************************/
function elysia_cron_admin_page() {
elysia_cron_initialize();
global $elysia_cron_settings, $elysia_cron_settings_by_context, $elysia_cron_current_context, $cron_completed, $cron_completed_time;
//dprint('Disabled: '._ec_variable_get('elysia_cron_disabled', false));
$v = _ec_variable_get('elysia_cron_disabled', false);
$output .= '
Global disable: '.($v ? 'YES':'no').'
';
$output .= 'Last context executed: '._ec_variable_get('elysia_cron_last_context', '').'
';
if (_ec_variable_get('elysia_cron_semaphore', 0))
$output .= 'Global semaphore active since '.elysia_cron_date(_ec_variable_get('elysia_cron_semaphore', 0)).'
';
$running = '';
foreach ($elysia_cron_settings_by_context as $context => $data)
if (elysia_cron_is_context_running($context))
$running .= $context.' ';
if ($running)
$output .= 'Running: '.$running.'
';
$output .= 'Last run: '.elysia_cron_date(_ec_variable_get('elysia_cron_last_run', 0)).'
';
foreach ($elysia_cron_settings_by_context as $context => $data) {
$running = elysia_cron_is_context_running($context);
$output .= ''.$context.($running ? ' (Running since '.elysia_cron_date($running).')' : '').($data['#data']['disabled'] ? ' (DISABLED)' : '').'
';
$output .= 'Last run: '.elysia_cron_date($data['#data']['last_run']).'
';
$output .= 'Last execution time: '.$data['#data']['last_execution_time'].'s (Shutdown: '.$data['#data']['last_shutdown_time'].'s) (Avg total: '.$data['#data']['avg_execution_time'].'s, Max total: '.$data['#data']['max_execution_time'].'s)
';
$output .= 'Execution count: '.$data['#data']['execution_count'].'
';
if ($data['#data']['last_aborted']) {
$output .= 'Last aborted: On function '.$data['#data']['last_abort_function'].'
';
}
if ($data['#data']['abort_count']) {
$output .= 'Abort count: '.$data['#data']['abort_count'].'
';
}
$rows = array();
foreach ($data as $job => $conf) if ($job != '#data') {
$rows[] = array(
($conf['disabled'] ? ''.$job.'' : (!$conf['running'] ? ''.$job.'' : ''.$job.' (R)')).(elysia_cron_should_run($conf) ? ' (w)' : ''),
array('data' => ''.elysia_cron_description($job).' '.
'['.l(t('run'), 'admin/build/cron/execute/'.$job, array('attributes' => array('onclick' => 'return confirm("'.t('Force execution of !job?', array('!job' => $job)).'");'))).']',
'colspan' => 4)
);
$rows[] = array(
$conf['rule'], elysia_cron_date($conf['last_run']), $conf['last_execution_time'].'s',
$conf['execution_count'], $conf['avg_execution_time'].'s / '.$conf['max_execution_time'].'s'
);
}
$output .= theme('table', array('Job / Rule', 'Last run', 'Last exec time', 'Exec count', 'Avg/Max Exec time'), $rows);
}
//dprint($elysia_cron_settings);
$output .= 'Notes: job times don\'t include shutdown times (only shown on channel times).
';
$output .= 'If an abort occours usually the job is not properly terminated, and so job timings can be inaccurate or wrong.
';
return $output;
}
function elysia_cron_reset_page() {
global $elysia_cron_settings, $elysia_cron_settings_by_context;
elysia_cron_initialize();
foreach ($elysia_cron_settings as $job => $conf) {
elysia_cron_set_job_stats($job, -1, 0, 0, 0, 0);
}
foreach ($elysia_cron_settings_by_context as $context => $conf) {
elysia_cron_set_context_stats($context, -1, 0, 0, 0, 0, 0, 0, 0, 0);
}
drupal_set_message(t('Reset done.'));
drupal_goto('admin/build/cron');
}
function elysia_cron_settings_form() {
global $elysia_cron_settings, $elysia_cron_settings_by_context;
elysia_cron_initialize();
$form = array();
$form['prefix_1'] = array(
'#type' => 'fieldset',
'#title' => t('Click for help and cron rules and script syntax'),
'#collapsible' => true,
'#collapsed' => true,
'#description' => <<Fields order
+---------------- minute (0 - 59)
| +------------- hour (0 - 23)
| | +---------- day of month (1 - 31)
| | | +------- month (1 - 12)
| | | | +---- day of week (0 - 7) (Sunday=0)
| | | | |
* * * * *
Each of the patterns from the first five fields may be either * (an asterisk),
which matches all legal values, or a list of elements separated by commas (see below).
For "day of the week" (field 5), 0 is considered Sunday, 6 is Saturday
(7 is an illegal value)
A job is executed when the time/date specification fields all match the current
time and date. There is one exception: if both "day of month" and "day of week"
are restricted (not "*"), then either the "day of month" field (3) or the "day of week"
field (5) must match the current day (even though the other of the two fields
need not match the current day).
Fields operators
There are several ways of specifying multiple date/time values in a field:
- The comma (',') operator specifies a list of values, for example: "1,3,4,7,8"
- The dash ('-') operator specifies a range of values, for example: "1-6", which is equivalent to "1,2,3,4,5,6"
- The asterisk ('*') operator specifies all possible values for a field. For example, an asterisk in the hour time field would be equivalent to 'every hour' (subject to matching other specified fields).
- The slash ('/') operator (called "step") can be used to skip a given number of values. For example, "*/3" in the hour time field is equivalent to "0,3,6,9,12,15,18,21".
Examples
*/15 * * * * : Execute job every 15 minutes
0 2,14 * * *: Execute job every day at 2:00 and 14:00
0 2 * * 1-5: Execute job at 2:00 of every working day
0 12 1 */2 1: Execute job every 2 month, at 12:00 of first day of the month OR at every monday.
Script
You can use the script section to easily create new jobs (by calling a php function)
or to change the scheduling of an existing job.
Every line of the script can be a comment (if it starts with #) or a job definition.
The syntax of a job definition is:
<-> [rule] <ctx:CONTEXT> [job]
(Tokens betweens [] are mandatory)
- <->: a line starting with "-" means that the job is DISABLED.
- [rule]: a crontab schedule rule. See above.
- <ctx:CONTEXT>: set the context of the job.
- [job]: could be the name of a supported job (for example: 'search_cron') or a function call, ending with ; (for example: 'process_queue();').
A comment on the line just preceding a job definition is considered the job description.
Remember that script OVERRIDES all settings on single jobs sections or context sections of the configuration
Examples of script
# Search indexing every 2 hours (i'm setting this as the job description)
0 */2 * * * search_cron
# I'll check for module status only on sunday nights
# (and this is will not be the job description, see the empty line below)
0 2 * * 0 update_status_cron
# Trackback ping process every 15min and on a channel called "net"
*/15 * * * * ctx:net trackback_cron
# Disable node_cron (i must set the cron rule even if disabled)
- */15 * * * * node_cron
# Launch function send_summary_mail('test@test.com', false); every night
# And set its description to "Send daily summary"
# Send daily summary
0 1 * * * send_summary_mail('test@test.com', false);
EOT
);
$form['prefix_2'] = array(
'#value' => '
'
);
$form['elysia_cron_disabled'] = array(
'#title' => t('Global disable'),
'#type' => 'checkbox',
'#default_value' => _ec_variable_get('elysia_cron_disabled', false),
);
$form['installation'] = array(
'#title' => t('Installation settings'),
'#type' => 'fieldset',
'#collapsible' => true,
'#collapsed' => true,
);
$form['installation']['cron_key'] = array(
'#title' => t('Cron key'),
'#type' => 'textfield',
'#default_value' => _ec_variable_get('cron_key', ''),
'#description' => t('This is used to avoid external cron calling. If you set this cron will by accessible only by calling http://site/cron.php?cron_key=XXX, so you\'ll need to modify system crontab to support this (Logged user with [administer elysia_cron] permission avoid this check).'),
);
$form['installation']['elysia_cron_allowed_hosts'] = array(
'#title' => t('Allowed hosts'),
'#type' => 'textfield',
'#default_value' => _ec_variable_get('elysia_cron_allowed_hosts', ''),
'#description' => t('Insert a list of ip addresses separated by , that can run cron.php (Logged user with [administer elysia_cron] permission avoid this check).'),
);
$form['installation']['elysia_cron_default_rule'] = array(
'#title' => t('Default schedule rule'),
'#type' => 'textfield',
'#default_value' => _ec_variable_get('elysia_cron_default_rule', false),
'#description' => t('If you don\'t specify a rule for a process, and if it has not a module specified one, this rule will apply'),
);
if (!ini_get('safe_mode'))
$form['installation']['elysia_cron_time_limit'] = array(
'#title' => t('Time limit'),
'#type' => 'textfield',
'#default_value' => _ec_variable_get('elysia_cron_time_limit', 240),
'#description' => t('Set the number of seconds a channel is allowed to run. If you have some jobs that needs more time to execute increase it or set to 0 to disable the limit (WARN: that way a stuck job will block the channel forever!).')
);
$form['installation']['elysia_cron_stuck_time'] = array(
'#title' => t('Stuck time'),
'#type' => 'textfield',
'#default_value' => _ec_variable_get('elysia_cron_stuck_time', 3600),
'#description' => t('How much seconds the process should wait to consider the job as stuck (so the channel can run again)'),
);
$form['installation']['elysia_cron_debug_messages'] = array(
'#title' => t('Debug'),
'#type' => 'select',
'#default_value' => _ec_variable_get('elysia_cron_debug_messages', 0),
'#options' => array(
0 => 'Disabled',
1 => 'Enabled',
),
'#description' => t('Enable extended logging (in watchdog)'),
);
$form['elysia_cron_script_fieldset'] = array(
'#title' => t('Script'),
'#type' => 'fieldset',
'#collapsible' => true,
'#collapsed' => !_ec_variable_get('elysia_cron_script', '')
);
$form['elysia_cron_script_fieldset']['elysia_cron_script'] = array(
'#type' => 'textarea',
'#rows' => 20,
'#default_value' => _ec_variable_get('elysia_cron_script', ''),
'#description' => t('You can specify new cron jobs or modify existing schedules by adding lines to the script.
'.
'Warning All rules specified in the script will OVERRIDE single job settings and contexts settings (sections below).')
);
$form['single_job'] = array(
'#title' => t('Single job settings'),
'#description' =>
''.t('Disabled').': '.t('Flag this to disable job execution').'
'.
''.t('Schedule rule').': '.t('Timing rule for the job. Leave empty to use default rule (shown after the field in parenthesis)').'
'.
''.t('Weight').': '.t('Use this to specify execution order: low weights are executed before high weights. Default value shown in parenthesis').'
'.
''.t('Context').': '.t('Specify a context for the job (create the context if not exists)').'
',
'#type' => 'fieldset',
'#collapsible' => true,
'#collapsed' => true,
);
$jobcontexts = array(
'#title' => t('Job context associations'),
'#description' => t('Leave empty for default context'),
'#type' => 'fieldset',
'#collapsible' => true,
'#collapsed' => true,
);
foreach ($elysia_cron_settings_by_context as $context => $cconf) foreach ($cconf as $job => $conf) if ($job != '#data' && !$conf['expression']) {
$form['single_job']['elysia_cron_'.$job] = array(
'#title' => $job, // t('Job !job', array('!job' => $job)),
'#description' => elysia_cron_description($job),
'#type' => 'fieldset',
'#collapsible' => true,
'#collapsed' => !elysia_cron_get_job_rule($job) && !elysia_cron_get_job_weight($job) && !elysia_cron_is_job_disabled($job) && !elysia_cron_get_job_context($job),
);
if (!$form['single_job']['elysia_cron_'.$job]['#collapsed'])
$form['single_job']['#collapsed'] = false;
$form['single_job']['elysia_cron_'.$job]['_elysia_cron_job_rule_'.$job] = array(
'#title' => t('Schedule rule'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => elysia_cron_get_job_rule($job),
'#description' => '('.$conf['default_rule'].')',
);
$form['single_job']['elysia_cron_'.$job]['_elysia_cron_job_weight_'.$job] = array(
'#title' => t('Weight'),
'#type' => 'textfield',
'#size' => 4,
'#default_value' => elysia_cron_get_job_weight($job),
'#description' => '('.$conf['default_weight'].')',
);
//$form['single_job']['elysia_cron_'.$job]['elysia_cron_'.$job.'_disabled'] = array(
$form['single_job']['elysia_cron_'.$job]['_elysia_cron_job_disabled_'.$job] = array(
'#title' => t('Disabled'),
'#type' => 'checkbox',
'#default_value' => elysia_cron_is_job_disabled($job, false),
);
//$jobcontexts['elysia_cron_'.$job.'_context'] = array(
$form['single_job']['elysia_cron_'.$job]['_elysia_cron_job_context_'.$job] = array(
'#title' => t('Context'), // t('Context for !job', array('!job' => $job)),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => elysia_cron_get_job_context($job),
);
if (elysia_cron_get_job_context($job))
$jobcontexts['#collapsed'] = false;
}
$form['contexts'] = array(
'#title' => t('Contexts settings'),
'#type' => 'fieldset',
'#collapsible' => true,
'#collapsed' => $jobcontexts['#collapsed'],
);
foreach ($elysia_cron_settings_by_context as $context => $conf) {
$form['contexts']['elysia_cron_ctx_'.$context] = array(
'#title' => $context, // t('Context !context', array('!context' => $context)),
'#type' => 'fieldset',
);
//$form['contexts']['elysia_cron_ctx_'.$context]['elysia_cron_ctx_'.$context.'_disabled'] = array(
$form['contexts']['elysia_cron_ctx_'.$context]['_elysia_cron_ctx_disabled_'.$context] = array(
'#title' => t('Disabled'),
'#type' => 'checkbox',
'#default_value' => elysia_cron_is_context_disabled($context, ''),
);
$form['contexts']['elysia_cron_ctx_'.$context]['_elysia_cron_ctx_rule_'.$context] = array(
'#title' => t('Default schedule rule'),
'#type' => 'textfield',
'#size' => 20,
'#default_value' => elysia_cron_get_context_rule($context),
);
if (elysia_cron_is_context_disabled($context))
$form['contexts']['#collapsed'] = false;
}
//$form['contexts']['jobcontexts'] = $jobcontexts;
$form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration') );
$form['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset to defaults') );
if (!empty($_POST) && form_get_errors()) {
drupal_set_message(t('The settings have not been saved because of the errors.'), 'error');
}
return $form;
}
function theme_elysia_cron_settings_form($form) {
$output = '';
$i = 0;
foreach (element_children($form['single_job']) as $c) {
$key = substr($c, 12);
//print_r($form['single_job'][$c]);
if ($i ++ == 0)
$output.=''.
''.$form['single_job'][$c]['_elysia_cron_job_disabled_'.$key]['#title'].' | '.
''.$form['single_job'][$c]['_elysia_cron_job_rule_'.$key]['#title'].' | '.
''.$form['single_job'][$c]['_elysia_cron_job_weight_'.$key]['#title'].' | '.
''.$form['single_job'][$c]['_elysia_cron_job_context_'.$key]['#title'].' | '.
'
';
$def_rule = $form['single_job'][$c]['_elysia_cron_job_rule_'.$key]['#description'];
$def_weight = $form['single_job'][$c]['_elysia_cron_job_weight_'.$key]['#description'];
$form['single_job'][$c]['_elysia_cron_job_rule_'.$key]['#title'] = NULL;
$form['single_job'][$c]['_elysia_cron_job_rule_'.$key]['#description'] = NULL;
$form['single_job'][$c]['_elysia_cron_job_rule_'.$key]['#attributes']['style'] = 'margin: 0';
$form['single_job'][$c]['_elysia_cron_job_weight_'.$key]['#title'] = NULL;
$form['single_job'][$c]['_elysia_cron_job_weight_'.$key]['#description'] = NULL;
$form['single_job'][$c]['_elysia_cron_job_weight_'.$key]['#attributes']['style'] = 'margin: 0';
$form['single_job'][$c]['_elysia_cron_job_disabled_'.$key]['#title'] = NULL;
$form['single_job'][$c]['_elysia_cron_job_disabled_'.$key]['#attributes']['style'] = 'margin: 0';
$form['single_job'][$c]['_elysia_cron_job_context_'.$key]['#title'] = NULL;
$form['single_job'][$c]['_elysia_cron_job_context_'.$key]['#attributes']['style'] = 'margin: 0';
$output .= ''.$form['single_job'][$c]['#title'].''.(($d = $form['single_job'][$c]['#description']) && $d != '-' ? ' (' . $d . ')' : '' ).' |
';
$output .= ''.
''.drupal_render($form['single_job'][$c]['_elysia_cron_job_disabled_'.$key]).' | '.
''.drupal_render($form['single_job'][$c]['_elysia_cron_job_rule_'.$key]).' | '.$def_rule.' | '.
''.drupal_render($form['single_job'][$c]['_elysia_cron_job_weight_'.$key]).' | '.$def_weight.' | '.
''.drupal_render($form['single_job'][$c]['_elysia_cron_job_context_'.$key]).' | '.
'
';
drupal_render($form['single_job'][$c]);
}
$output .= '
';
$form['single_job'][] = array('#type' => 'markup', '#value' => $output);
$output = '';
$i = 0;
foreach (element_children($form['contexts']) as $c) {
$key = substr($c, 16);
if ($i ++ == 0)
$output.=''.
''.t('Name').' | '.
''.$form['contexts'][$c]['_elysia_cron_ctx_disabled_'.$key]['#title'].' | '.
''.$form['contexts'][$c]['_elysia_cron_ctx_rule_'.$key]['#title'].' | '.
'
';
$form['contexts'][$c]['_elysia_cron_ctx_disabled_'.$key]['#title'] = NULL;
$form['contexts'][$c]['_elysia_cron_ctx_disabled_'.$key]['#attributes']['style'] = 'margin: 0';
$form['contexts'][$c]['_elysia_cron_ctx_rule_'.$key]['#title'] = NULL;
$form['contexts'][$c]['_elysia_cron_ctx_rule_'.$key]['#attributes']['style'] = 'margin: 0';
$output .= ''.
''.$form['contexts'][$c]['#title'].' | '.
''.drupal_render($form['contexts'][$c]['_elysia_cron_ctx_disabled_'.$key]).' | '.
''.drupal_render($form['contexts'][$c]['_elysia_cron_ctx_rule_'.$key]).' | '.
'
';
drupal_render($form['contexts'][$c]);
}
$output .= '
';
$form['contexts'][] = array('#type' => 'markup', '#value' => $output);
return drupal_render($form);
}
function elysia_cron_settings_form_validate($form, $form_state) {
global $elysia_cron_settings;
$form_values = $form_state['values'];
$script = $form_values['elysia_cron_script'];
if ($script) {
$errors = elysia_cron_decode_script($script, false);
if ($errors) {
form_set_error('elysia_cron_script', t('Invalid lines:').implode('
', $errors));
}
}
foreach ($form_values as $key => $value) {
if ($value && preg_match('/^_elysia_cron_([^_]+_[^_]+)_(.*)$/', $key, $r) && ($r[1] == 'job_rule' || $r[1] == 'ctx_rule')) {
if (!preg_match('/^\\s*([0-9*,\/-]+[ ]+[0-9*,\/-]+[ ]+[0-9*,\/-]+[ ]+[0-9*,\/-]+[ ]+[0-9*,\/-]+)\\s*$/', $value))
form_set_error($key, t('Invalid rule: !rule', array('!rule' => $value)));
}
}
}
function elysia_cron_settings_form_submit($form, &$form_state) {
$form_values = $form_state['values'];
$op = isset($form_values['op']) ? $form_values['op'] : '';
// Exclude unnecessary elements.
unset($form_values['submit'], $form_values['reset'], $form_values['form_id'], $form_values['op'], $form_values['form_token']);
foreach ($form_values as $key => $value) {
$value = trim($value);
if (!preg_match('/^_elysia_cron_([^_]+_[^_]+)_(.*)$/', $key, $r)) {
if ($op == t('Reset to defaults') || !$value) {
_ec_variable_del($key);
if ($key == 'cron_key')
variable_del($key);
} else {
if (is_array($value) && isset($form_values['array_filter']))
$value = array_keys(array_filter($value));
_ec_variable_set($key, $value);
if ($key == 'cron_key')
variable_set($key, $value);
}
} else {
if ($op == t('Reset to defaults') || !$value) {
switch ($r[1]) {
case 'job_context': elysia_cron_reset_job_context($r[2]); break;
case 'job_rule': elysia_cron_reset_job_rule($r[2]); break;
case 'job_weight': elysia_cron_reset_job_weight($r[2]); break;
case 'job_disabled': elysia_cron_reset_job_disabled($r[2]); break;
case 'ctx_disabled': elysia_cron_reset_context_disabled($r[2]); break;
case 'ctx_rule': elysia_cron_reset_context_rule($r[2]); break;
}
} elseif ($value) {
switch ($r[1]) {
case 'job_context': elysia_cron_set_job_context($r[2], $value); break;
case 'job_rule': elysia_cron_set_job_rule($r[2], $value); break;
case 'job_weight': elysia_cron_set_job_weight($r[2], $value); break;
case 'job_disabled': elysia_cron_set_job_disabled($r[2], $value); break;
case 'ctx_disabled': elysia_cron_set_context_disabled($r[2], $value); break;
case 'ctx_rule': elysia_cron_set_context_rule($r[2], $value); break;
}
}
}
}
if ($op == t('Reset to defaults')) {
drupal_set_message(t('The configuration options have been reset to their default values.'));
}
else {
drupal_set_message(t('The configuration options have been saved.'));
}
}
function elysia_cron_date($timestamp) {
// 'd/m/Y H:i'
return date(variable_get('date_format_short', 'm/d/Y - H:i'), $timestamp);
}
function elysia_cron_execute_page($job = false) {
global $cron_completed, $cron_executing_job;
if (!$job) {
drupal_set_message(t('No job specified'), 'error');
drupal_goto('admin/build/cron');
}
$running = false;
if (elysia_cron_is_job_running($job, false)) {
if (time() - elysia_cron_get_job_last_run($job, 0) > _ec_variable_get('elysia_cron_stuck_time', 3600))
drupal_set_message(t('Job already running, but is probably stuck, so i consider it as terminated'));
else {
drupal_set_message(t('Job already running'));
$running = true;
}
}
if (!$running) {
$cron_completed = false;
$cron_executing_job = $job;
// Register shutdown callback
register_shutdown_function('elysia_cron_execute_page_cleanup');
elysia_cron_execute($job);
$cron_completed = true;
drupal_set_message(t('Job executed'));
}
drupal_goto('admin/build/cron');
}
function elysia_cron_execute_page_cleanup() {
global $cron_completed, $cron_executing_job;
if ($cron_completed) return;
// See if the semaphore is still locked.
if (elysia_cron_is_job_running($cron_executing_job, false)) {
watchdog('cron', t('Unexpected temination of cron job %job manually started, aborted.', array('%job' => $cron_executing_job)), WATCHDOG_WARNING);
elysia_cron_set_job_running($cron_executing_job, false);
}
}
/*******************************************************************************
* INTERNAL
******************************************************************************/
function elysia_cron_decode_script($text, $apply = true) {
global $elysia_cron_settings;
$lines = explode("\n", $text);
$lastcomment = '';
$errors = array();
$conf = array();
foreach ($lines as $line) {
$line = trim($line);
if (!empty($line)) {
if ($line{0} == '#') {
$lastcomment = trim(substr($line, 1));
} else if (preg_match('/^(-[ ]*|)([0-9*,\/-]+[ ]+[0-9*,\/-]+[ ]+[0-9*,\/-]+[ ]+[0-9*,\/-]+[ ]+[0-9*,\/-]+)[ ]+(ctx:([a-zA-Z0-9_-]+)[ ]+|)([^(:]+)(\(.*\);|)$/', $line, $r)) {
$c = array(
'disabled' => !empty($r[1]),
'rule' => $r[2],
'description' => $lastcomment,
'context' => $r[4] ? $r[4] : 'default',
);
$lastcomment = '';
if (empty($r[6])) {
if (!isset($elysia_cron_settings[$r[5]])) {
// Referring a module function that not exists
$errors[] = $line;
continue;
}
$name = $r[5];
} else {
// custom expression, generate a unique name
$r[5];
$postfix = '';
while (isset($elysia_cron_settings[$r[5].$postfix]))
$postfix = ($postfix?$postfix:0) + 1;
$name = $r[5].$postfix;
$c['expression'] = $r[5].$r[6];
}
if ($apply)
$elysia_cron_settings[$name] = isset($elysia_cron_settings[$name]) ? array_merge($elysia_cron_settings[$name], $c) : $c;
} else {
$errors[] = $line;
}
} else {
$lastcomment = '';
}
}
return count($errors) ? $errors : false;
}
function elysia_cron_module_jobs() {
$jobs = array();
foreach (module_implements('cron') as $module)
if ($module != 'elysia_cron')
$jobs[] = array($module, $module .'_cron');
foreach (module_implements('cronapi') as $module) {
$fn = $module.'_cronapi';
$l = $fn('list');
if (is_array($l)) foreach ($l as $job => $desc)
if (!in_array($job, $jobs))
$jobs[] = array($module, $job);
}
return $jobs;
}
function elysia_cron_initialize($skipscript = false) {
elysia_cron_check_version_update();
global $elysia_cron_settings, $elysia_cron_settings_by_context;
$elysia_cron_settings = array();
$elysia_cron_settings_by_context = array();
foreach (elysia_cron_module_jobs() as $job) {
$context = elysia_cron_get_job_context($job[1], 'default');
$defrule = false;
$fn = $job[0].'_cronapi';
if (function_exists($fn))
$defrule = $fn('rule', $job[1]);
if (!$defrule) {
$defrule = elysia_cron_get_context_rule($context);
if (!$defrule)
$defrule = _ec_variable_get('elysia_cron_default_rule', '45 * * * *');
}
$rule = elysia_cron_get_job_rule($job[1]);
if (!$rule)
$rule = $defrule;
if (function_exists($fn)) {
$defweight = $fn('weight', $job[1]);
if (!is_numeric($defweight))
$defweight = 0;
} else
$defweight = 0;
$elysia_cron_settings[$job[1]] = array(
'key' => $job[1],
'context' => $context,
'module' => $job[0],
'rule' => $rule,
'default_rule' => $defrule,
'weight' => elysia_cron_get_job_weight($job[1], $default_weight),
'default_weight' => $defweight,
'disabled' => elysia_cron_is_job_disabled($job[1]),
'running' => elysia_cron_is_job_running($job[1]),
);
}
if (!$skipscript) {
$script = _ec_variable_get('elysia_cron_script', false);
if ($script)
elysia_cron_decode_script($script);
}
uasort($elysia_cron_settings, '_elysia_cron_sort');
foreach ($elysia_cron_settings as $job => &$conf) {
$stats = elysia_cron_get_job_stats($job);
foreach ($stats as $sk => $sv)
$conf[$sk] = $sv;
$elysia_cron_settings_by_context[$conf['context']][$job] = &$elysia_cron_settings[$job];
}
foreach ($elysia_cron_settings_by_context as $context => $data) {
uasort($elysia_cron_settings_by_context[$context], '_elysia_cron_sort');
$elysia_cron_settings_by_context[$context]['#data'] = elysia_cron_get_context_stats($context);
$elysia_cron_settings_by_context[$context]['#data']['disabled'] = elysia_cron_is_context_disabled($context);
}
}
function _elysia_cron_sort($a, $b) {
if ($a['weight'] == $b['weight'])
return strcmp($a['key'], $b['key']);
return $a['weight'] - $b['weight'];
}
function elysia_cron_run($manual_run = false) {
global $conf;
ignore_user_abort(true);
if (!$manual_run) {
$cron_key = _ec_variable_get('cron_key', '');
if ($cron_key && !user_access('administer elysia_cron') && $_GET['cron_key'] != $cron_key)
return;
$allowed_hosts = _ec_variable_get('elysia_cron_allowed_hosts', false);
if ($allowed_hosts && !user_access('administer elysia_cron') && !in_array(ip_address(), explode(",", $allowed_hosts)))
return;
}
_ec_variable_set('elysia_cron_last_run', time());
_ec_variable_set('cron_last', time());
if (_ec_variable_get('elysia_cron_disabled', false))
return;
// Global Semaphore to avoid concurrent execution of code below
$semglob = _ec_variable_get('elysia_cron_semaphore', 0);
if ($semglob && (time() - $semglob > 120)) {
watchdog('cron', 'Global semaphore has been active for more that 2 minutes, probably stuck, reset.', array(), WATCHDOG_ERROR);
_ec_variable_del('elysia_cron_semaphore');
unset($semglob);
}
if ($semglob)
return;
_ec_variable_set('elysia_cron_semaphore', time());
if (!ini_get('safe_mode')) {
set_time_limit(_ec_variable_get('elysia_cron_time_limit', 240));
}
elysia_cron_initialize();
global $elysia_cron_settings, $elysia_cron_settings_by_context, $elysia_cron_current_context, $cron_completed, $cron_completed_time;
$contexts = array_keys($elysia_cron_settings_by_context);
$context = _ec_variable_get('elysia_cron_last_context', '');
$i = array_search($context, $contexts);
if ($i === FALSE) $i = -1;
$k = 0;
$stuck_time = _ec_variable_get('elysia_cron_stuck_time', 3600);
for ($j = ($i + 1) % count($contexts); $k < count($contexts); $j = ($j + 1) % count($contexts)) {
$sem = elysia_cron_is_context_running($contexts[$j]);
if ($sem && (time() - $sem > $stuck_time)) {
watchdog('cron', 'Cron context (%context) has been running for more than an %stuck_time secs and is most likely stuck.', array('%context' => $contexts[$j], '%stuck_time' => $stuck_time), WATCHDOG_ERROR);
//$elysia_cron_settings_by_context[$contexts[$j]]['#data']['running'] = false;
elysia_cron_set_context_running($contexts[$j], false);
//_ec_variable_del('elysia_cron_ctx_'.$contexts[$j].'_running');
unset($sem);
elysia_cron_execute_aborted($contexts[$j]);
}
if (!$sem && !$elysia_cron_settings_by_context[$contexts[$j]]['#data']['disabled'] && time() - $elysia_cron_settings_by_context[$contexts[$j]]['#data']['last_run'] > 60) {
$jobs = elysia_cron_active_jobs($contexts[$j]);
if (count($jobs)) break;
}
$k++;
}
if (!$jobs || !count($jobs)) {
_ec_variable_del('elysia_cron_semaphore');
return;
}
// Some modules (feedapi, ipaper...) uses the internal "cron_semaphore" variable to detect
// start time of cron process. I'll set this only in memory for that purpose.
// (In normal drupal cron execution that is done by a variable_set just before this call,
// but i need to set this manually if drupal cron is bypassed)
$conf['cron_semaphore'] = time();
$elysia_cron_current_context = $contexts[$j];
_ec_variable_set('elysia_cron_last_context', $elysia_cron_current_context);
if (_ec_variable_get('elysia_cron_debug_messages', 0))
watchdog('cron', 'Cron context %context run started.', array('%context' => $elysia_cron_current_context), WATCHDOG_NOTICE);
// Register shutdown callback
register_shutdown_function('elysia_cron_cleanup');
// Lock cron semaphore
elysia_cron_set_context_running($elysia_cron_current_context, time());
elysia_cron_set_context_last_run($elysia_cron_current_context, time());
_ec_variable_del('elysia_cron_semaphore');
foreach ($jobs as $job) {
$job_running = false;
if (elysia_cron_is_job_running($job, false)) {
if (time() - elysia_cron_get_job_last_run($job, 0) > _ec_variable_get('elysia_cron_stuck_time', 3600))
watchdog('cron', 'Job %job is already running, but is probably stuck, so i consider it as terminated', array('%job' => $job), WATCHDOG_NOTICE);
else {
watchdog('cron', 'Job %job is already running', array('%job' => $job), WATCHDOG_NOTICE);
$job_running = true;
}
}
if (!$job_running)
elysia_cron_execute($job);
}
$cron_completed = true;
$cron_completed_time = time();
// Cron is really completed after shutdown functions
register_shutdown_function('elysia_cron_completed');
// Terminate drupal cron
_ec_variable_del('cron_semaphore');
$conf['cron_semaphore'] = false;
// Return TRUE so other functions can check if it did run successfully
return TRUE;
}
function elysia_cron_execute($job) {
global $elysia_cron_settings;
$time = time();
elysia_cron_set_job_last_run($job, $time);
elysia_cron_set_job_running($job, false);
try {
if ($elysia_cron_settings[$job]['expression']) {
eval($elysia_cron_settings[$job]['expression']);
} if (function_exists($job)) {
//sleep(5);
$job();
} else {
$fn = $elysia_cron_settings[$job]['module'].'_cronapi';
if (function_exists($fn)) {
$fn('execute', $job);
} else {
watchdog('cron', 'Execution of %job failed, can\'t find function!', array('%job' => $job), WATCHDOG_ERROR);
}
}
} catch (Exception $e) {
watchdog('cron', 'Exception: %e', array('%e' => $e), WATCHDOG_ERROR);
$exception = true;
//TODO Manage it
}
$stats = elysia_cron_get_job_stats($job);
$time = time() - $time;
elysia_cron_set_job_stats($job,
-1,
$time,
($c = $stats['execution_count'] + 1),
round((($stats['avg_execution_time'] * ($c - 1)) + $time) / $c, 2),
$time > $stats['max_execution_time'] ? $time : -1
);
elysia_cron_set_job_running($job, false);
}
function elysia_cron_execute_aborted($context) {
global $elysia_cron_settings_by_context;
elysia_cron_set_context_running($context, false);
elysia_cron_set_context_last_aborted($context, true);
elysia_cron_set_context_abort_count($context, elysia_cron_get_context_abort_count($context) + 1);
foreach ($elysia_cron_settings_by_context[$context] as $job => $conf) if ($job != '#data') {
if (elysia_cron_is_job_running($job)) {
elysia_cron_set_context_last_abort_function($context, $job);
elysia_cron_set_job_running($job, false);
}
}
}
/**
* Shutdown function for cron cleanup.
*
* Used for unexpected termination of code.
*/
function elysia_cron_cleanup() {
global $elysia_cron_settings, $elysia_cron_current_context, $cron_completed, $cron_completed_time;
if ($cron_completed) return;
// See if the semaphore is still locked.
if (elysia_cron_is_context_running($elysia_cron_current_context)) {
watchdog('cron', 'Unexpected temination of cron context %context, aborted.', array('%context' => $elysia_cron_current_context), WATCHDOG_WARNING);
elysia_cron_execute_aborted($elysia_cron_current_context);
}
}
/**
* Successful termination (after all shutdown hooks invoked by cron functions).
*/
function elysia_cron_completed() {
global $elysia_cron_settings, $elysia_cron_current_context, $cron_completed, $cron_completed_time;
//dprint("completed ".$elysia_cron_current_context);
// Record cron time
_ec_variable_set('cron_last', time());
if (_ec_variable_get('elysia_cron_debug_messages', 0))
watchdog('cron', 'Cron context %context run completed.', array('%context' => $elysia_cron_current_context), WATCHDOG_NOTICE);
$stats = elysia_cron_get_context_stats($elysia_cron_current_context);
$time = time() - $stats['last_run'];
elysia_cron_set_context_stats($elysia_cron_current_context,
-1,
$time,
($c = $stats['execution_count'] + 1),
round((($stats['avg_execution_time'] * ($c - 1)) + $time) / $c, 2),
$time > $stats['max_execution_time'] ? $time : -1,
time() - $cron_completed_time,
false
);
elysia_cron_set_context_running($elysia_cron_current_context, false);
}
/**
* Get all jobs that needs to be executed in a context
*/
function elysia_cron_active_jobs($context) {
global $elysia_cron_settings_by_context;
$jobs = array();
foreach ($elysia_cron_settings_by_context[$context] as $job => $conf) if ($job != '#data') {
if (elysia_cron_should_run($conf))
$jobs[] = $job;
}
return $jobs;
}
// Used by elysia_cron_should_run
function _cronT($time, $d = -1, $h = -1, $m = -1) {
if ($d < 0)
//return date('n', $now) * 31 * 24 * 60 + date('w', $now) * 24 * 60 + date('H', $now) * 60 + date('i', $now);
return date('n', $time) * 31 * 24 * 60 + date('j', $time) * 24 * 60 + date('H', $time) * 60 + date('i', $time);
else
return $time * 31 * 24 * 60 + $d * 24 * 60 + $h * 60 + $m;
}
// Used by elysia_cron_should_run
function _cronMonDaysFromWeekDays($year, $mon, $weekdays) {
$result = array();
for ($i = 1; checkdate($mon, $i, $year); $i++) {
$w = date('w', mktime(12, 00, 00, $mon, $i, $year));
if (in_array($w, $weekdays))
$result[] = $i;
}
return $result;
}
// Used by elysia_cron_should_run
function _cronDecodeRule($rule, $min, $max) {
if ($rule == '*')
return range($min, $max);
$result = array();
foreach (explode(',', $rule) as $token) {
if (preg_match('/^([0-9]+)-([0-9]+)$/', $token, $r)) {
$result = array_merge($result, range($r[1], $r[2]));
}
elseif (preg_match('/^\*\/([0-9]+)$/', $token, $r)) {
for ($i = $min; $i <= $max; $i++)
if ($i % $r[1] == 0)
$result[] = $i;
}
elseif (is_numeric($token)) {
$result[] = $token;
}
}
return $result;
}
function elysia_cron_should_run($conf, $now = -1) {
if ($conf['disabled'])
return false;
if ($now < 0)
$now = time();
if ((!$conf['last_run']) || ($now - $conf['last_run'] > 365 * 86400))
return true;
if (!preg_match('/^([0-9*,\/-]+)[ ]+([0-9*,\/-]+)[ ]+([0-9*,\/-]+)[ ]+([0-9*,\/-]+)[ ]+([0-9*,\/-]+)$/', $conf['rule'], $rules)) {
watchdog('cron', 'Invalid rule found: %rule', array('%rule' => $conf['rule']));
return false;
}
$weekdayspec = ($rules[5] != '*');
$mondayspec = ($rules[3] != '*');
$rules[5] = _cronDecodeRule($rules[5], 0, 6);
$rules[4] = _cronDecodeRule($rules[4], 1, 12);
$rules[3] = _cronDecodeRule($rules[3], 1, 31);
$rules[2] = _cronDecodeRule($rules[2], 0, 23);
$rules[1] = _cronDecodeRule($rules[1], 0, 59);
$lastT = _cronT($conf['last_run'] + 30);
$nowT = _cronT($now);
$nowTDelta = $nowT - $lastT + ($lastT > $nowT ? 12 * 31 * 24 * 60 : 0);
$year = date('Y',$conf['last_run']);
if ($mondayspec || (!$mondayspec && !$weekdayspec)) {
$first = -1;
foreach ($rules[4] as $mon)
foreach ($rules[3] as $d) if (checkdate($mon, $d, $year))
foreach ($rules[2] as $h)
foreach ($rules[1] as $m) {
$t = _cronT($mon, $d, $h, $m);
//dprint("* ".$t." L:".$lastT);
if ($first < 0) $first = $t;
if ($t > $lastT) {
$nextT = $t;
break 4;
}
}
//dprint("R: ".$nextT);
if (!$nextT)
$nextT = $first;
$nextTDelta = $nextT - $lastT + ($lastT > $nextT ? 12 * 31 * 24 * 60 : 0);
//dprint($nowT.' '.$nowTDelta.' '.$nextT.' '.$nextTDelta);
if ($nowTDelta >= $nextTDelta)
return true;
}
if ($weekdayspec) {
foreach ($rules[4] as $mon)
foreach (_cronMonDaysFromWeekDays($year, $mon, $rules[5]) as $d)
foreach ($rules[2] as $h)
foreach ($rules[1] as $m) {
$t = _cronT($mon, $d, $h, $m);
//dprint("* ".$t." L:".$lastT);
if ($t > $lastT) {
$nextT = $t;
break 4;
}
}
//dprint("R: ".$nextT);
if (!$nextT) {
//Must get the first of following year (if day_of_week is specified it's not the same of previous one)
foreach ($rules[4] as $mon)
foreach (_cronMonDaysFromWeekDays($year + 1, $mon, $rules[5]) as $d)
foreach ($rules[2] as $h)
foreach ($rules[1] as $m) {
$nextT = _cronT($mon, $d, $h, $m);
break 4;
}
}
$nextTDelta = $nextT - $lastT + ($lastT > $nextT ? 12 * 31 * 24 * 60 : 0);
//dprint($nowT.' '.$nowTDelta.' '.$nextT.' '.$nextTDelta);
if ($nowTDelta >= $nextTDelta)
return true;
}
return false;
}
/**
* Obtain job description
*/
function elysia_cron_description($job) {
global $elysia_cron_settings;
if ($elysia_cron_settings[$job]['description'])
return $elysia_cron_settings[$job]['description'];
$fn = $elysia_cron_settings[$job]['module'].'_cronapi';
if (function_exists($fn)) {
$l = $fn('list');
if ($l[$job])
return $l[$job];
}
return theme('elysia_cron_description', $job);
}
/*******************************************************************************
* TESTS
******************************************************************************/
function test_elysia_cron_should_run() {
dprint("Start test");
//mktime: hr min sec mon day yr
dprint(" 1.".(false == elysia_cron_should_run(array('rule' => '0 12 * * *', 'last_run' => mktime(12, 0, 0, 1, 2, 2008)), mktime(12, 01, 0, 1, 2, 2008))));
dprint(" 2.".(false == elysia_cron_should_run(array('rule' => '0 12 * * *', 'last_run' => mktime(12, 0, 0, 1, 2, 2008)), mktime(15, 00, 0, 1, 2, 2008))));
dprint(" 3.".(false == elysia_cron_should_run(array('rule' => '0 12 * * *', 'last_run' => mktime(12, 0, 0, 1, 2, 2008)), mktime(11, 59, 0, 1, 3, 2008))));
dprint(" 4.".(true == elysia_cron_should_run(array('rule' => '0 12 * * *', 'last_run' => mktime(12, 0, 0, 1, 2, 2008)), mktime(12, 00, 0, 1, 3, 2008))));
dprint(" 5.".(false == elysia_cron_should_run(array('rule' => '59 23 * * *', 'last_run' => mktime(23, 59, 0, 1, 2, 2008)), mktime(0, 00, 0, 1, 3, 2008))));
dprint(" 6.".(true == elysia_cron_should_run(array('rule' => '59 23 * * *', 'last_run' => mktime(23, 59, 0, 1, 2, 2008)), mktime(23, 59, 0, 1, 3, 2008))));
dprint(" 7.".(true == elysia_cron_should_run(array('rule' => '59 23 * * *', 'last_run' => mktime(23, 59, 0, 1, 2, 2008)), mktime(0, 00, 0, 1, 4, 2008))));
dprint(" 8.".(true == elysia_cron_should_run(array('rule' => '59 23 * * *', 'last_run' => mktime(23, 58, 0, 1, 2, 2008)), mktime(23, 59, 0, 1, 2, 2008))));
dprint(" 9.".(true == elysia_cron_should_run(array('rule' => '59 23 * * *', 'last_run' => mktime(23, 58, 0, 1, 2, 2008)), mktime(0, 0, 0, 1, 3, 2008))));
dprint("10.".(false == elysia_cron_should_run(array('rule' => '59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 5, 2008))));
dprint("11.".(false == elysia_cron_should_run(array('rule' => '59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(0, 0, 0, 1, 6, 2008))));
dprint("12.".(true == elysia_cron_should_run(array('rule' => '59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 6, 2008))));
dprint("13.".(true == elysia_cron_should_run(array('rule' => '59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(00, 00, 0, 1, 7, 2008))));
dprint("14.".(true == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(23, 29, 0, 1, 6, 2008))));
dprint("15.".(true == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 6, 2008))));
dprint("16.".(false == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 5, 2008))));
dprint("17.".(true == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(23, 58, 0, 1, 6, 2008))));
dprint("18.".(false == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 58, 0, 1, 5, 2008)), mktime(23, 28, 0, 1, 6, 2008))));
dprint("19.".(false == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 28, 0, 1, 5, 2008)), mktime(23, 29, 0, 1, 5, 2008))));
dprint("20.".(false == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 28, 0, 1, 5, 2008)), mktime(23, 30, 0, 1, 5, 2008))));
dprint("21.".(false == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 28, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 5, 2008))));
dprint("22.".(true == elysia_cron_should_run(array('rule' => '29,59 23 * * 0', 'last_run' => mktime(23, 28, 0, 1, 5, 2008)), mktime(23, 29, 0, 1, 6, 2008))));
dprint("23.".(false == elysia_cron_should_run(array('rule' => '29,59 23 * * 5', 'last_run' => mktime(23, 59, 0, 2, 22, 2008)), mktime(23, 59, 0, 2, 28, 2008))));
dprint("24.".(true == elysia_cron_should_run(array('rule' => '29,59 23 * * 5', 'last_run' => mktime(23, 59, 0, 2, 22, 2008)), mktime(23, 59, 0, 2, 29, 2008))));
dprint("25.".(true == elysia_cron_should_run(array('rule' => '29,59 23 * * 5', 'last_run' => mktime(23, 59, 0, 2, 22, 2008)), mktime(0, 0, 0, 3, 1, 2008))));
dprint("26.".(false == elysia_cron_should_run(array('rule' => '59 23 * * 3', 'last_run' => mktime(23, 59, 0, 12, 31, 2008)), mktime(0, 0, 0, 1, 1, 2009))));
dprint("27.".(false == elysia_cron_should_run(array('rule' => '59 23 * * 3', 'last_run' => mktime(23, 59, 0, 12, 31, 2008)), mktime(0, 0, 0, 1, 7, 2009))));
dprint("28.".(true == elysia_cron_should_run(array('rule' => '59 23 * * 3', 'last_run' => mktime(23, 59, 0, 12, 31, 2008)), mktime(23, 59, 0, 1, 7, 2009))));
dprint("29.".(true == elysia_cron_should_run(array('rule' => '59 23 * 2 5', 'last_run' => mktime(23, 59, 0, 2, 22, 2008)), mktime(23, 59, 0, 2, 29, 2008))));
dprint("30.".(true == elysia_cron_should_run(array('rule' => '59 23 * 2 5', 'last_run' => mktime(23, 59, 0, 2, 22, 2008)), mktime(0, 0, 0, 3, 1, 2008))));
dprint("31.".(false == elysia_cron_should_run(array('rule' => '59 23 * 2 5', 'last_run' => mktime(23, 59, 0, 2, 29, 2008)), mktime(23, 59, 0, 3, 7, 2008))));
dprint("32.".(false == elysia_cron_should_run(array('rule' => '59 23 * 2 5', 'last_run' => mktime(23, 59, 0, 2, 29, 2008)), mktime(23, 58, 0, 2, 6, 2009))));
dprint("33.".(true == elysia_cron_should_run(array('rule' => '59 23 * 2 5', 'last_run' => mktime(23, 59, 0, 2, 29, 2008)), mktime(23, 59, 0, 2, 6, 2009))));
dprint("34.".(true == elysia_cron_should_run(array('rule' => '59 23 *'.'/10 * *', 'last_run' => mktime(23, 58, 0, 1, 10, 2008)), mktime(23, 59, 0, 1, 10, 2008))));
dprint("35.".(false == elysia_cron_should_run(array('rule' => '59 23 *'.'/10 * *', 'last_run' => mktime(23, 59, 0, 1, 10, 2008)), mktime(23, 59, 0, 1, 11, 2008))));
dprint("36.".(true == elysia_cron_should_run(array('rule' => '59 23 *'.'/10 * *', 'last_run' => mktime(23, 59, 0, 1, 10, 2008)), mktime(23, 59, 0, 1, 20, 2008))));
dprint("37.".(true == elysia_cron_should_run(array('rule' => '59 23 1-5,10-15 * *', 'last_run' => mktime(23, 59, 0, 1, 4, 2008)), mktime(23, 59, 0, 1, 5, 2008))));
dprint("38.".(true == elysia_cron_should_run(array('rule' => '59 23 1-5,10-15 * *', 'last_run' => mktime(23, 59, 0, 1, 4, 2008)), mktime(23, 59, 0, 1, 6, 2008))));
dprint("39.".(false == elysia_cron_should_run(array('rule' => '59 23 1-5,10-15 * *', 'last_run' => mktime(23, 59, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 6, 2008))));
dprint("40.".(false == elysia_cron_should_run(array('rule' => '59 23 1-5,10-15 * *', 'last_run' => mktime(23, 59, 0, 1, 5, 2008)), mktime(23, 58, 0, 1, 10, 2008))));
dprint("41.".(true == elysia_cron_should_run(array('rule' => '59 23 1-5,10-15 * *', 'last_run' => mktime(23, 59, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 10, 2008))));
dprint("42.".(true == elysia_cron_should_run(array('rule' => '59 23 1-5,10-15 * *', 'last_run' => mktime(23, 59, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 16, 2008))));
dprint("43.".(true == elysia_cron_should_run(array('rule' => '59 23 1-5 1 0', 'last_run' => mktime(23, 59, 0, 1, 4, 2008)), mktime(23, 59, 0, 1, 5, 2008))));
dprint("44.".(true == elysia_cron_should_run(array('rule' => '59 23 1-5 1 0', 'last_run' => mktime(23, 59, 0, 1, 5, 2008)), mktime(23, 59, 0, 1, 6, 2008))));
dprint("45.".(false == elysia_cron_should_run(array('rule' => '59 23 1-5 1 0', 'last_run' => mktime(23, 59, 0, 1, 6, 2008)), mktime(23, 59, 0, 1, 7, 2008))));
dprint("46.".(true == elysia_cron_should_run(array('rule' => '59 23 1-5 1 0', 'last_run' => mktime(23, 59, 0, 1, 6, 2008)), mktime(23, 59, 0, 1, 13, 2008))));
dprint("47.".(false == elysia_cron_should_run(array('rule' => '59 23 1-5 1 0', 'last_run' => mktime(23, 59, 0, 2, 4, 2008)), mktime(23, 59, 0, 2, 5, 2008))));
dprint("48.".(false == elysia_cron_should_run(array('rule' => '59 23 1-5 1 0', 'last_run' => mktime(23, 59, 0, 2, 5, 2008)), mktime(23, 59, 0, 2, 10, 2008))));
dprint("49.".(false == elysia_cron_should_run(array('rule' => '59 23 1-5 1 0', 'last_run' => mktime(23, 59, 0, 2, 10, 2008)), mktime(23, 59, 0, 2, 17, 2008))));
dprint("End test");
}
// Remove comment to run tests
// test_elysia_cron_should_run();
/*******************************************************************************
* THEMING
******************************************************************************/
/**
* Implementation of hook_theme().
*/
function elysia_cron_theme(){
return array(
'elysia_cron_description' => array(
'arguments' => array( 'job' => NULL ),
),
'elysia_cron_settings_form' => array(
'arguments' => array( 'form' => NULL ),
),
);
}
/**
* You can theme this function to provide your description for cron functions, if they do not provide one.
*/
function theme_elysia_cron_description($job) {
switch($job) {
case 'search_cron': return 'Update search database index';
case 'activitystream_cron': return 'Fetch RSS feeds and web calls for activitystream';
case 'mailhandler_cron': return 'Fetch POP3/IMAP accounts managed by MailHandler';
case 'watchdog_cron': return 'Remove expired log messages and flood control events';
case 'filter_cron': return 'Expire outdated filter cache entries';
case 'node_cron': return 'History table cleanup';
case 'system_cron': return 'Expire cache_path';
case 'aggregation_cron': return 'Fetch RSS feeds for aggregation module';
case 'amazon_cron': return 'Refresh Amazon products';
case 'image_cron': return 'Deletes old temp images';
case 'persistent_login_cron': return 'Expire persistent login';
case 'trackback_cron': return 'Process trackback ping queue';
case 'update_status_cron': return 'Checks for drupal module updates (Note: own frequency check ignore cron rules)';
case 'user_karma_cron': return 'User karma expiration / rebuild (Note: own frequency check ignore cron rules)';
case 'votingapi_cron': return 'Update votes (if not configured for immediate calculation)';
case 'statistics_cron': return 'Reset day counts / Clean expired access logs';
case 'googleanalytics_cron': return 'Delete cached version of ga.js/urchin.js.';
case 'xmlsitemap_cron': return 'XML sitemap ping.';
case 'xmlsitemap_node_cron': return 'Update XML sitemap with new nodes';
case 'xmlsitemap_term_cron': return 'Update XML sitemap with new terms';
case 'lm_paypal_cron': return 'Remove old IPN records';
case 'user_import_cron': return 'Continue partial imports';
default: return '-';
}
}