MENU_DEFAULT_LOCAL_TASK, 'title' => 'Settings', 'weight' => 1, ); $items['admin/config/system/batch'] = array( 'title' => 'Batch', 'description' => 'Administer batch jobs', 'page callback' => 'drupal_get_form', 'page arguments' => array('background_batch_settings_form'), 'access arguments' => array('administer site'), 'file' => 'background_batch.pages.inc', ); $items['admin/config/system/batch/overview'] = array( 'type' => MENU_LOCAL_TASK, 'title' => 'Overview', 'description' => 'Batch job overview', 'page callback' => 'background_batch_overview_page', 'access arguments' => array('administer site'), 'file' => 'background_batch.pages.inc', 'weight' => 3, ); return $items; } /** * Implements hook_menu_alter(). */ function background_batch_menu_alter(&$items) { $items['batch'] = array( 'page callback' => 'background_batch_page', 'access callback' => TRUE, 'theme callback' => '_system_batch_theme', 'type' => MENU_CALLBACK, 'file' => 'background_batch.pages.inc', 'module' => 'background_batch', ); } /** * Implements hook_batch_alter(). * Steal the operation and hook into context data. */ function background_batch_batch_alter(&$batch) { if ($batch['progressive'] && $batch['url'] == 'batch') { foreach ($batch['sets'] as &$set) { if (!empty($set['operations'])) { foreach ($set['operations'] as &$operation) { $operation = array('_background_batch_operation', array($operation)); } } } $batch['timestamp'] = microtime(TRUE); } // In order to make this batch session independend we save the owner UID. global $user; $batch['uid'] = $user->uid; } /** * Implements hook_library(). */ function background_batch_library() { $libraries = array(); $libraries['background-process.batch'] = array( 'title' => 'Background batch API', 'version' => '1.0.0', 'js' => array( drupal_get_path('module', 'background_batch') . '/js/batch.js' => array('group' => JS_DEFAULT, 'cache' => FALSE), ), 'dependencies' => array( array('background_batch', 'background-process.progress'), ), ); $libraries['background-process.progress'] = array( 'title' => 'Background batch progress', 'version' => VERSION, 'js' => array( drupal_get_path('module', 'background_batch') . '/js/progress.js' => array('group' => JS_DEFAULT, 'cache' => FALSE), ), ); return $libraries; } /** * Run a batch operation with "listening" context. * @param $operation * Batch operation definition. * @param &$context * Context for the batch operation. */ function _background_batch_operation($operation, &$context) { // Steal context and trap finished variable $fine_progress = !empty($context['sandbox']['background_batch_fine_progress']); if ($fine_progress) { $batch_context = new BackgroundBatchContext($context); } else { $batch_context = $context; } // Call the original operation $operation[1][] = &$batch_context; call_user_func_array($operation[0], $operation[1]); if ($fine_progress) { // Transfer back context result to batch api $batch_context = (array)$batch_context; foreach (array_keys($batch_context) as $key) { $context[$key] = $batch_context[$key]; } } else { $batch_context = new BackgroundBatchContext($context); $batch_context['finished'] = $context['finished']; } } /** * Process a batch step * @param type $id * @return type */ function _background_batch_process($id = NULL) { if (!$id) { return; } // Retrieve the current state of batch from db. $data = db_query("SELECT batch FROM {batch} WHERE bid = :bid", array(':bid' => $id))->fetchColumn(); if (!$data) { return; } require_once('includes/batch.inc'); $batch =& batch_get(); $batch = unserialize($data); // Check if the current user owns (has access to) this batch. global $user; if ($batch['uid'] != $user->uid) { return drupal_access_denied(); } // Register database update for the end of processing. drupal_register_shutdown_function('_batch_shutdown'); timer_start('background_batch_processing'); $percentage = 0; $mem_max_used = 0; $mem_last_used = memory_get_usage(); $mem_limit = ini_get('memory_limit'); preg_match('/(\d+)(\w)/', $mem_limit, $matches); switch ($matches[2]) { case 'M': default: $mem_limit = $matches[1] * 1024 * 1024; break; } while ($percentage < 100) { list ($percentage, $message) = _batch_process(); $mem_used = memory_get_usage(); // If we memory usage of last run will exceed the memory limit in next run // then bail out if ($mem_limit < $mem_used + $mem_last_used) { break; } $mem_last_used = $mem_used - $mem_last_used; // If we maximum memory usage of previous runs will exceed the memory limit in next run // then bail out $mem_max_used = $mem_max_used < $mem_last_used ? $mem_last_used : $mem_max_used; if ($mem_limit < $mem_used + $mem_max_used) { break; } // Restart background process after X miliseconds if (timer_read('background_batch_processing') > variable_get('background_batch_process_lifespan', BACKGROUND_BATCH_PROCESS_LIFESPAN)) { break; } } if ($percentage < 100) { background_process_keepalive($id); } } /** * Processes the batch. * * Unless the batch has been marked with 'progressive' = FALSE, the function * issues a drupal_goto and thus ends page execution. * * This function is not needed in form submit handlers; Form API takes care * of batches that were set during form submission. * * @param $redirect * (optional) Path to redirect to when the batch has finished processing. * @param $url * (optional - should only be used for separate scripts like update.php) * URL of the batch processing page. */ function background_batch_process_batch($redirect = NULL, $url = 'batch', $redirect_callback = 'drupal_goto') { $batch =& batch_get(); drupal_theme_initialize(); if (isset($batch)) { // Add process information $process_info = array( 'current_set' => 0, 'progressive' => TRUE, 'url' => $url, 'url_options' => array(), 'source_url' => $_GET['q'], 'redirect' => $redirect, 'theme' => $GLOBALS['theme_key'], 'redirect_callback' => $redirect_callback, ); $batch += $process_info; // The batch is now completely built. Allow other modules to make changes // to the batch so that it is easier to reuse batch processes in other // environments. drupal_alter('batch', $batch); // Assign an arbitrary id: don't rely on a serial column in the 'batch' // table, since non-progressive batches skip database storage completely. $batch['id'] = db_next_id(); // Move operations to a job queue. Non-progressive batches will use a // memory-based queue. foreach ($batch['sets'] as $key => $batch_set) { _batch_populate_queue($batch, $key); } // Initiate processing. // Now that we have a batch id, we can generate the redirection link in // the generic error message. $t = get_t(); $batch['error_message'] = $t('Please continue to the error page', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished'))))); // Clear the way for the drupal_goto() redirection to the batch processing // page, by saving and unsetting the 'destination', if there is any. if (isset($_GET['destination'])) { $batch['destination'] = $_GET['destination']; unset($_GET['destination']); } // Store the batch. db_insert('batch') ->fields(array( 'bid' => $batch['id'], 'timestamp' => REQUEST_TIME, 'token' => drupal_get_token($batch['id']), 'batch' => serialize($batch), )) ->execute(); // Set the batch number in the session to guarantee that it will stay alive. $_SESSION['batches'][$batch['id']] = TRUE; // Redirect for processing. $function = $batch['redirect_callback']; if (function_exists($function)) { // $function($batch['url'], array('query' => array('op' => 'start', 'id' => $batch['id']))); } } background_process_start('_background_batch_process_callback', $batch); } function _background_batch_process_callback($batch) { $rbatch =& batch_get(); $rbatch = $batch; require_once('background_batch.pages.inc'); _background_batch_page_start(); } /** * Class batch context. * Automatically updates progress when 'finished' index is changed. */ class BackgroundBatchContext extends ArrayObject { private $batch = NULL; private $interval = NULL; private $progress = NULL; public function __construct() { $this->interval = variable_get('background_batch_delay', BACKGROUND_BATCH_DELAY) / 1000000; $args = func_get_args(); return call_user_func_array(array('parent', '__construct'), $args); } /** * Set progress update interval in seconds (float). */ public function setInterval($interval) { $this->interval = $interval; } /** * Override offsetSet(). * Update progress if needed. */ public function offsetSet($name, $value) { if ($name == 'finished') { if (!isset($this->batch)) { $this->batch =& batch_get(); $this->progress = progress_get_progress('_background_batch:' . $this->batch['id']); } if ($this->batch) { $total = $this->batch['sets'][$this->batch['current_set']]['total']; $count = $this->batch['sets'][$this->batch['current_set']]['count']; $elapsed = $this->batch['sets'][$this->batch['current_set']]['elapsed']; $progress_message = $this->batch['sets'][$this->batch['current_set']]['progress_message']; $current = $total - $count; $step = 1 / $total; $base = $current * $step; $progress = $base + $value * $step; progress_estimate_completion($this->progress); $elapsed = floor($this->progress->current - $this->progress->start); $values = array( '@remaining' => $count, '@total' => $total, '@current' => $current, '@percentage' => $progress * 100, '@elapsed' => format_interval($elapsed), // If possible, estimate remaining processing time. '@estimate' => format_interval(floor($this->progress->estimate) - floor($this->progress->current)), ); $message = strtr($progress_message, $values); $message .= $message && $this['message'] ? '
' : ''; $message .= $this['message']; progress_set_intervalled_progress('_background_batch:' . $this->batch['id'], $message ? $message : $this->progress->message, $progress, $this->interval); } } return parent::offsetSet($name, $value); } }