diff --git a/commands/core/drupal/batch.inc b/commands/core/drupal/batch.inc index 41dee24..1c11b24 100644 --- a/commands/core/drupal/batch.inc +++ b/commands/core/drupal/batch.inc @@ -14,8 +14,7 @@ * The command to call to process the batch. * */ -function _drush_backend_batch_process($command = 'batch-process', $args) { - global $user; +function _drush_backend_batch_process($command = 'batch-process', $args, $options) { $batch =& batch_get(); if (isset($batch)) { @@ -31,6 +30,7 @@ function _drush_backend_batch_process($command = 'batch-process', $args) { // 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(); + $args[] = $batch['id']; $batch['progressive'] = TRUE; @@ -141,6 +141,9 @@ function _drush_batch_worker() { 'finished' => &$finished, 'message' => &$task_message, ); + // Magic wrap to catch changes to 'message' key. + // TODO: DrushBatchContext seems to work pretty well for some things, but it breaks updatedb in site-upgrade (and therefore may be a problem for some pm-updatecode + updatedb as well?) + $batch_context = new DrushBatchContext($batch_context); call_user_func_array($function, array_merge($args, array(&$batch_context))); if ($finished == 1) { diff --git a/commands/core/drupal/batch_6.inc b/commands/core/drupal/batch_6.inc index 3396253..cebfeb4 100644 --- a/commands/core/drupal/batch_6.inc +++ b/commands/core/drupal/batch_6.inc @@ -14,8 +14,7 @@ * The command to call to process the batch. * */ -function _drush_backend_batch_process($command = 'batch-process', $args) { - global $user; +function _drush_backend_batch_process($command = 'batch-process', $args, $options) { $batch =& batch_get(); if (isset($batch)) { @@ -28,6 +27,7 @@ function _drush_backend_batch_process($command = 'batch-process', $args) { // at least an empty string for the (not null) 'token' column. db_query("INSERT INTO {batch} (token, timestamp) VALUES ('', %d)", time()); $batch['id'] = db_last_insert_id('batch', 'bid'); + $args[] = $batch['id']; // Actually store the batch data and the token generated form the batch id. db_query("UPDATE {batch} SET token = '%s', batch = '%s' WHERE bid = %d", drupal_get_token($batch['id']), serialize($batch), $batch['id']); @@ -35,13 +35,7 @@ function _drush_backend_batch_process($command = 'batch-process', $args) { $finished = FALSE; while (!$finished) { - if ($user->uid) { - $data = drush_invoke_process('@self', $command, $args, array($batch['id'], '-u', $user->uid)); - } - else { - $data = drush_invoke_process('@self', $command, $args, array($batch['id'])); - } - + $data = drush_invoke_process('@self', $command, $args, $options); $finished = drush_get_error() || !$data || (isset($data['context']['drush_batch_process_finished']) && $data['context']['drush_batch_process_finished'] == TRUE); } } @@ -105,6 +99,8 @@ function _drush_batch_worker() { // Build the 'context' array, execute the function call, // and retrieve the user message. $batch_context = array('sandbox' => &$current_set['sandbox'], 'results' => &$current_set['results'], 'finished' => &$finished, 'message' => &$task_message); + // Magic wrap to catch changes to 'message' key. + $batch_context = new DrushBatchContext($batch_context); // Process the current operation. call_user_func_array($function, array_merge($args, array(&$batch_context))); } diff --git a/includes/backend.inc b/includes/backend.inc index f3b09e4..ef16e1a 100644 --- a/includes/backend.inc +++ b/includes/backend.inc @@ -541,7 +541,7 @@ function drush_backend_invoke_sitealias_command($site_record, $command, $args, $ $drush_command_path = drush_build_drush_command($drush_path, array_key_exists('php', $command_options) ? $command_options['php'] : NULL, drush_os($site_record), array_key_exists('remote-host', $site_record)); // Insure that contexts such as DRUSH_SIMULATE and NO_COLOR are included. - $command_options += _drush_backend_get_global_contexts(); + $command_options += _drush_backend_get_global_contexts($site_record); list($post_options, $commandline_options, $drush_global_options) = _drush_backend_classify_options($command_options, $backend_options); $cmd = _drush_backend_generate_command($site_record, $drush_command_path . " " . _drush_backend_argument_string($drush_global_options, $os) . " " . $command, $args, $commandline_options, $backend_options) . ' 2>&1'; @@ -552,14 +552,18 @@ function drush_backend_invoke_sitealias_command($site_record, $command, $args, $ * Find all of the drush contexts that are used to cache global values and * return them in an associative array. */ -function _drush_backend_get_global_contexts() { +function _drush_backend_get_global_contexts($site_record) { $result = array(); $global_option_list = drush_get_global_options(FALSE); foreach ($global_option_list as $global_key => $global_metadata) { if ((is_array($global_metadata)) && (array_key_exists('context', $global_metadata))) { - $value = drush_get_context($global_metadata['context'], array()); - if (!empty($value)) { - $result[$global_key] = $value; + // If the context is declared to be a 'local-context-only', + // then only put it in if this is a local dispatch. + if (TRUE || !array_key_exists('local-context-only', $global_metadata) || !array_key_exists('remot-host', $site_record)) { + $value = drush_get_context($global_metadata['context'], array()); + if (!empty($value)) { + $result[$global_key] = $value; + } } } } diff --git a/includes/batch.inc b/includes/batch.inc index 5260046..2d09bde 100644 --- a/includes/batch.inc +++ b/includes/batch.inc @@ -22,9 +22,28 @@ * Each major release of Drupal has also had slightly different implementations * of the batch API, and this provides a uniform interface to all of these * implementations. - * */ +/** + * Class extending ArrayObject to allow the batch API to perform logging when + * some keys of the array change. + * + * It is used to wrap batch's $context array and set log messages when values + * are assigned to keys 'message' or 'error_message'. + * + * @see _drush_batch_worker(). + */ +class DrushBatchContext extends ArrayObject { + function offsetSet($name, $value) { + if ($name == 'message') { + drush_log($value, 'ok'); + } + elseif ($name == 'error_message') { + drush_log($value, 'error'); + } + parent::offsetSet($name, $value); + } +} /** * Process a Drupal batch by spawning multiple Drush processes. @@ -48,9 +67,15 @@ * use their own command. * */ -function drush_backend_batch_process($command = 'batch-process', $args = array()) { +function drush_backend_batch_process($command = 'batch-process', $args = array(), $options = array()) { + // Command line options to pass to the command. + global $user; + if ($user->uid) { + $options['u'] = $user->uid; + } + drush_include_engine('drupal', 'batch', drush_drupal_major_version()); - _drush_backend_batch_process($command, $args); + _drush_backend_batch_process($command, $args, $options); } /** diff --git a/includes/command.inc b/includes/command.inc index e919a23..22a8308 100644 --- a/includes/command.inc +++ b/includes/command.inc @@ -1003,6 +1003,8 @@ function _drush_find_commandfiles($phase, $phase_max = FALSE) { // User commands, specified by 'include' option if ($include = drush_get_option(array('i', 'include'), FALSE)) { + // We set the DRUSH_INCLUDE context here for the benefit of backend_invoke + drush_set_context('DRUSH_INCLUDE', $include); foreach (explode(PATH_SEPARATOR, $include) as $path) { $searchpath[] = $path; } diff --git a/includes/drush.inc b/includes/drush.inc index fb1d2e8..d70df32 100644 --- a/includes/drush.inc +++ b/includes/drush.inc @@ -321,8 +321,8 @@ function _convert_csv_to_array($args) { * - description: The help text for this item. displayed by `drush help`. */ function drush_get_global_options($brief = FALSE) { - $options['root'] = array('short-form' => 'r', 'context' => 'DRUSH_DRUPAL_ROOT', 'never-post' => TRUE, 'description' => "Drupal root directory to use (default: current directory).", 'example-value' => ''); - $options['uri'] = array('short-form' => 'l', 'context' => 'DRUSH_URI', 'never-post' => TRUE, 'description' => 'URI of the drupal site to use (only needed in multisite environments or when running on an alternate port).', 'example-value' => 'http://example.com:8888'); + $options['root'] = array('short-form' => 'r', 'never-post' => TRUE, 'description' => "Drupal root directory to use (default: current directory).", 'example-value' => ''); + $options['uri'] = array('short-form' => 'l', 'never-post' => TRUE, 'description' => 'URI of the drupal site to use (only needed in multisite environments or when running on an alternate port).', 'example-value' => 'http://example.com:8888'); $options['verbose'] = array('short-form' => 'v', 'context' => 'DRUSH_VERBOSE', 'description' => 'Display extra information about the command.'); $options['debug'] = array('short-form' => 'd', 'context' => 'DRUSH_DEBUG', 'description' => 'Display even more information, including internal messages.'); $options['yes'] = array('short-form' => 'y', 'context' => 'DRUSH_AFFIRMATIVE', 'description' => "Assume 'yes' as answer to all prompts."); @@ -336,7 +336,7 @@ function drush_get_global_options($brief = FALSE) { if (!$brief) { $options['quiet'] = array('short-form' => 'q', 'description' => 'Suppress non-error messages.'); - $options['include'] = array('short-form' => 'i', 'description' => "A list of additional directory paths to search for drush commands."); + $options['include'] = array('short-form' => 'i', 'context' => 'DRUSH_INCLUDE', 'local-context-only' => TRUE, 'description' => "A list of additional directory paths to search for drush commands."); $options['config'] = array('short-form' => 'c', 'description' => "Specify an additional config file to load. See example.drushrc.php."); $options['user'] = array('short-form' => 'u', 'description' => "Specify a Drupal user to login with. May be a name or a number."); $options['cache'] = array('description' => 'Keep a local cache of downloaded files.'); diff --git a/tests/backendTest.php b/tests/backendTest.php index f3c5e9a..bb6fab7 100644 --- a/tests/backendTest.php +++ b/tests/backendTest.php @@ -14,7 +14,6 @@ */ class backendCase extends Drush_CommandTestCase { - const DRUSH_BACKEND_OUTPUT_DELIMITER = 'DRUSH_BACKEND_OUTPUT_START>>>%s<<'sql')); $exec = sprintf('echo %s | %s help --backend 2>/dev/null', self::escapeshellarg($stdin), self::escapeshellarg(UNISH_DRUSH)); $this->execute($exec); - $parsed = $this->parse($this->getOutput()); + $parsed = parse_backend_output($this->getOutput()); $this->assertTrue((bool) $parsed, 'Successfully parsed backend output'); $this->assertArrayHasKey('log', $parsed); $this->assertArrayHasKey('output', $parsed); @@ -56,7 +55,7 @@ class backendCase extends Drush_CommandTestCase { // Check error propogation by requesting an invalid command (missing Drupal site). $exec = sprintf('%s core-cron --backend 2>/dev/null', self::escapeshellarg(UNISH_DRUSH)); $this->execute($exec, self::EXIT_ERROR); - $parsed = $this->parse($this->getOutput()); + $parsed = parse_backend_output($this->getOutput()); $this->assertEquals(1, $parsed['error_status']); $this->assertArrayHasKey('DRUSH_NO_DRUPAL_ROOT', $parsed['error_log']); } @@ -79,26 +78,4 @@ class backendCase extends Drush_CommandTestCase { $this->assertTrue($backend_output_offset !== FALSE, "Drush backend output marker appears in output."); $this->assertTrue($drush_version_offset < $backend_output_offset, "Drush version string appears in output before the backend output marker."); } - - /* - * A slightly less functional copy of drush_backend_parse_output(). - */ - function parse($string) { - $regex = sprintf(self::DRUSH_BACKEND_OUTPUT_DELIMITER, '(.*)'); - preg_match("/$regex/s", $string, $match); - if ($match[1]) { - // we have our JSON encoded string - $output = $match[1]; - // remove the match we just made and any non printing characters - $string = trim(str_replace(sprintf(self::DRUSH_BACKEND_OUTPUT_DELIMITER, $match[1]), '', $string)); - } - - if ($output) { - $data = json_decode($output, TRUE); - if (is_array($data)) { - return $data; - } - } - return $string; - } } diff --git a/tests/completeTest.php b/tests/completeTest.php index 57c29d1..f43c2a0 100644 --- a/tests/completeTest.php +++ b/tests/completeTest.php @@ -25,17 +25,17 @@ class completeCase extends Drush_CommandTestCase { $this->drush('php-eval', array('drush_complete_cache_clear();')); // Confirm we get cache rebuilds for runs both in and out of a site // which is expected since these should resolve to separate cache IDs. - $this->verifyComplete('@dev uni', 'uninstall', 'unit-invoke', FALSE); + $this->verifyComplete('@dev uni', 'uninstall', 'unit-batch', FALSE); $this->verifyComplete('uni', 'uninstall', 'uninstall', FALSE); // Next, rerun and check results to confirm cache IDs are generated // correctly on our fast bootstrap when returning the cached result. - $this->verifyComplete('@dev uni', 'uninstall', 'unit-invoke'); + $this->verifyComplete('@dev uni', 'uninstall', 'unit-batch'); $this->verifyComplete('uni', 'uninstall', 'uninstall'); // Test cache clearing for a completion type, which should be effective only // for current environment - i.e. a specific site should not be effected. $this->drush('php-eval', array('drush_complete_cache_clear("command-names");')); - $this->verifyComplete('@dev uni', 'uninstall', 'unit-invoke'); + $this->verifyComplete('@dev uni', 'uninstall', 'unit-batch'); $this->verifyComplete('uni', 'uninstall', 'uninstall', FALSE); // Test cache clearing for a command specific completion type, which should @@ -62,7 +62,7 @@ class completeCase extends Drush_CommandTestCase { // Commands that start the same as another command (i.e. unit is a valid // command, but we should still list unit-eval and unit-invoke when // completing on "unit"). - $this->verifyComplete('@dev unit', 'unit', 'unit-invoke'); + $this->verifyComplete('@dev unit', 'unit', 'unit-batch'); // Global option alone. $this->verifyComplete('--n', '--no', '--nocolor'); // Site alias + command. @@ -108,8 +108,10 @@ class completeCase extends Drush_CommandTestCase { $exec = sprintf('%s --early=includes/complete.inc --complete-debug %s %s 2> %s', UNISH_DRUSH, UNISH_DRUSH, $command, $debug_file); $this->execute($exec); $result = $this->getOutputAsList(); - $this->assertEquals($first, reset($result)); - $this->assertEquals($last, end($result)); + $expected = reset($result); + $this->assertEquals("$command: $first", "$command: $expected"); + $expected = end($result); + $this->assertEquals("$command: $last", "$command: $expected"); // If checking for HIT, we ensure no MISS exists, if checking for MISS we // ensure no HIT exists. However, we exclude the first cache report, since // it is expected that the command-names cache (loaded when matching @@ -124,4 +126,4 @@ class completeCase extends Drush_CommandTestCase { $this->assertFalse(strpos($contents, 'Cache ' . $check_not_exist . ' cid', $first_cache_pos)); unlink($debug_file); } -} \ No newline at end of file +} diff --git a/tests/drush_testcase.inc b/tests/drush_testcase.inc index a731b89..33926b6 100644 --- a/tests/drush_testcase.inc +++ b/tests/drush_testcase.inc @@ -424,6 +424,29 @@ function unish_init() { return rmdir($dir); } +/** + * A slightly less functional copy of drush_backend_parse_output(). + */ +define('DRUSH_BACKEND_OUTPUT_DELIMITER', 'DRUSH_BACKEND_OUTPUT_START>>>%s<< 'Return an array indicating which invoke hooks got called.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, ); + $items['unit-batch'] = array( + 'description' => 'Run a batch process.', + 'bootstrap' => DRUSH_BOOTSTRAP_MAX, + ); return $items; } @@ -69,3 +73,40 @@ function unit_invoke_log($function = NULL) { return $called; } } + +/** + * Command callback. + */ +function drush_unit_batch() { + // Reduce php memory/time limits to test backend respawn. + // TODO. + + $batch = array( + 'operations' => array( + array('_drush_unit_batch_operation', array()), + ), + 'finished' => '_drush_unit_batch_finished', + // 'file' => Doesn't work for us. Drupal 7 enforces this path + // to be relative to DRUPAL_ROOT. + // @see _batch_process(). + ); + batch_set($batch); + drush_backend_batch_process(); + + // Print the batch output. + drush_backend_output(); +} + +function _drush_unit_batch_operation(&$context) { + $context['message'] = "!!! ArrayObject does its job."; + + for ($i = 0; $i < 5; $i++) { + drush_print("Iteration $i"); + } + $context['finished'] = 1; +} + +function _drush_unit_batch_finished() { + // Restore php limits. + // TODO. +}