Skip to content

Commit

Permalink
Merge pull request #2996 from drush-ops/backend-stdin
Browse files Browse the repository at this point in the history
Read options from stdin in backend POST mode.
  • Loading branch information
greg-1-anderson authored Oct 1, 2017
2 parents 8e0638b + cedb946 commit 5b7c28e
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 27 deletions.
25 changes: 24 additions & 1 deletion src/Preflight/LessStrictArgvInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class LessStrictArgvInput extends ArgvInput
{
private $tokens;
private $parsed;
protected $additionalOptions = [];

/**
* Constructor.
Expand All @@ -36,7 +37,21 @@ public function __construct(array $argv = null, InputDefinition $definition = nu
// strip the application name
array_shift($this->tokens);

// parent::__construct($argv, $definition);
parent::__construct($argv, $definition);
}

/**
* {@inheritdoc}
*/
public function getOption($name)
{
if (array_key_exists($name, $this->options)) {
return $this->options[$name];
}
if ($this->definition->hasOption($name)) {
return $this->definition->getOption($name)->getDefault();
}
return false;
}

protected function setTokens(array $tokens)
Expand Down Expand Up @@ -64,6 +79,8 @@ protected function parse()
$this->parseArgument($token);
}
}
// Put back any options that were injected.
$this->options += $this->additionalOptions;
}

/**
Expand Down Expand Up @@ -188,6 +205,12 @@ private function addShortOption($shortcut, $value)
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
}

public function injectAdditionalOptions($additionalOptions)
{
$this->additionalOptions += $additionalOptions;
$this->options += $additionalOptions;
}

/**
* Adds a long option value.
*
Expand Down
17 changes: 8 additions & 9 deletions src/Preflight/Preflight.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,11 @@ protected function doRun($argv)
// Start code coverage
$this->startCoverage($preflightArgs);

// TODO: Should we allow config to set values defined by preflightArgs?
// (e.g. --root and --uri).
// Maybe preflight args should be one of the config layers, and we
// should fetch 'root' et. al. from config rather than preflight args.
// Get the config files provided by prepareConfig()
$config = $configLocator->config();

// Copy items from the preflight args into configuration
// Copy items from the preflight args into configuration.
// This will also load certain config values into the preflight args.
$preflightArgs->applyToConfig($config);

// Determine the local site targeted, if any.
Expand All @@ -198,12 +196,13 @@ protected function doRun($argv)
$configLocator->addSitewideConfig($root);
$configLocator->setComposerRoot($this->selectedComposerRoot());

// Look up the locations where alias files may be found.
$paths = $configLocator->getSiteAliasPaths($preflightArgs, $this->environment);

// Configure alias manager.
$aliasManager = (new SiteAliasManager())->addSearchLocations($paths);
$selfAliasRecord = $aliasManager->findSelf($preflightArgs, $this->environment, $root);
$aliasConfig = $selfAliasRecord->exportConfig();
$configLocator->addAliasConfig($aliasConfig);
$configLocator->addAliasConfig($selfAliasRecord->exportConfig());

// Process the selected alias. This might change the selected site,
// so we will add new site-wide config location for the new root.
Expand All @@ -213,7 +212,6 @@ protected function doRun($argv)
// a site-local Drush. If there is, we will redispatch to it.
// NOTE: termination handlers have not been set yet, so it is okay
// to exit early without taking special action.

$status = RedispatchToSiteLocal::redispatchIfSiteLocalDrush($argv, $root, $this->environment->vendorPath());
if ($status !== false) {
return $status;
Expand All @@ -225,7 +223,6 @@ protected function doRun($argv)

// Remember the paths to all the files we loaded, so that we can
// report on it from Drush status or wherever else it may be needed.

$config->set('runtime.config.paths', $configLocator->configFilePaths());

// We need to check the php minimum version again, in case anyone
Expand All @@ -235,6 +232,8 @@ protected function doRun($argv)
// Find all of the available commandfiles, save for those that are
// provided by modules in the selected site; those will be added
// during bootstrap.
// TODO: Move to ConfigLocator::getCommandFilePaths for consistency
// with ConfigLocator::GetSiteAliasPaths().
$commandfileSearchpath = $this->findCommandFileSearchPath($preflightArgs, $root);

// Require the Composer autoloader for Drupal (if different)
Expand Down
93 changes: 85 additions & 8 deletions src/Preflight/PreflightArgs.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,6 @@ public function __construct($data = [])
parent::__construct($data + [self::STRICT => true]);
}

public function createInput()
{
if ($this->isStrict()) {
return new ArgvInput($this->args());
}
return new LessStrictArgvInput($this->args());
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -334,4 +326,89 @@ function ($item) {
)
);
}

/**
* Create a Symfony Input object
*/
public function createInput()
{
// In strict mode (the default), create an ArgvInput. When
// strict mode is disabled, create a more forgiving input object.
if ($this->isStrict() && !$this->isBackend()) {
return new ArgvInput($this->args());
}

// If in backend mode, read additional options from stdin.
// TODO: Maybe reading stdin options should be the responsibilty of some
// backend manager class? Could be called from preflight and injected here.
$input = new LessStrictArgvInput($this->args());
$input->injectAdditionalOptions($this->readStdinOptions());

return $input;
}

/**
* Read options fron STDIN during POST requests.
*
* This function will read any text from the STDIN pipe,
* and attempts to generate an associative array if valid
* JSON was received.
*
* @return
* An associative array of options, if successfull. Otherwise an empty array.
*/
protected function readStdinOptions()
{
// If we move this method to a backend manager, then testing for
// backend mode will be the responsibility of the caller.
if (!$this->isBackend()) {
return [];
}

$fp = fopen('php://stdin', 'r');
// Windows workaround: we cannot count on stream_get_contents to
// return if STDIN is reading from the keyboard. We will therefore
// check to see if there are already characters waiting on the
// stream (as there always should be, if this is a backend call),
// and if there are not, then we will exit.
// This code prevents drush from hanging forever when called with
// --backend from the commandline; however, overall it is still
// a futile effort, as it does not seem that backend invoke can
// successfully write data to that this function can read,
// so the argument list and command always come out empty. :(
// Perhaps stream_get_contents is the problem, and we should use
// the technique described here:
// http://bugs.php.net/bug.php?id=30154
// n.b. the code in that issue passes '0' for the timeout in stream_select
// in a loop, which is not recommended.
// Note that the following DOES work:
// drush ev 'print(json_encode(array("test" => "XYZZY")));' | drush status --backend
// So, redirecting input is okay, it is just the proc_open that is a problem.
if (drush_is_windows()) {
// Note that stream_select uses reference parameters, so we need variables (can't pass a constant NULL)
$read = array($fp);
$write = null;
$except = null;
// Question: might we need to wait a bit for STDIN to be ready,
// even if the process that called us immediately writes our parameters?
// Passing '100' for the timeout here causes us to hang indefinitely
// when called from the shell.
$changed_streams = stream_select($read, $write, $except, 0);
// Return on error or no changed streams (0).
// Oh, according to http://php.net/manual/en/function.stream-select.php,
// stream_select will return FALSE for streams returned by proc_open.
// That is not applicable to us, is it? Our stream is connected to a stream
// created by proc_open, but is not a stream returned by proc_open.
if ($changed_streams < 1) {
return [];
}
}
stream_set_blocking($fp, false);
$string = stream_get_contents($fp);
fclose($fp);
if (trim($string)) {
return json_decode($string, true);
}
return [];
}
}
13 changes: 4 additions & 9 deletions tests/backendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,10 @@ function testNonExistentCommand()
* - JSON object is wrapped in expected delimiters.
*/
function testTarget() {
// Without --strict=0, the version call would fail.
// Now, strict is not supported; we will see how this behaves without it.
// Backend invoke always runs in non-strict mode now.
$stdin = json_encode([]);
$exec = sprintf('%s version --not-exist --backend', self::getDrush());
$this->execute($exec, self::EXIT_ERROR, NULL, NULL, $stdin);
$this->execute($exec, self::EXIT_SUCCESS, NULL, NULL, $stdin);
$exec = sprintf('%s version --backend', self::getDrush());
$this->execute($exec, self::EXIT_SUCCESS, NULL, NULL, $stdin);
$parsed = $this->parse_backend_output($this->getOutput());
Expand Down Expand Up @@ -213,7 +212,6 @@ function testBackendMethodGet() {
* backend invoke.
*/
function testBackendMethodPost() {
$this->markTestIncomplete('Depends on reading from stdin');
$options = array(
'backend' => NULL,
'include' => dirname(__FILE__), // Find unit.drush.inc commandfile.
Expand All @@ -222,13 +220,10 @@ function testBackendMethodPost() {
$this->drush('php-eval', array($php), $options);
$parsed = $this->parse_backend_output($this->getOutput());
// assert that $parsed has 'x' and 'data'
$this->assertEquals('y', $parsed['object']['x']);
$this->assertEquals(array (
'x' => 'y',
'data' =>
array (
'a' => 1,
'b' => 2,
),
), $parsed['object']);
), $parsed['object']['data']);
}
}

0 comments on commit 5b7c28e

Please sign in to comment.