MaxSem has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/399768 )
Change subject: Allow programmatic input in Command
......................................................................
Allow programmatic input in Command
Bug: T182463
Change-Id: Ib68180c7af12558686f4864c24fd85f01201d6fb
---
M includes/shell/Command.php
M includes/shell/Shell.php
M tests/phpunit/includes/shell/CommandTest.php
3 files changed, 78 insertions(+), 28 deletions(-)
Approvals:
MaxSem: Looks good to me, approved
jenkins-bot: Verified
diff --git a/includes/shell/Command.php b/includes/shell/Command.php
index f887ada..4f65e4d 100644
--- a/includes/shell/Command.php
+++ b/includes/shell/Command.php
@@ -56,6 +56,9 @@
/** @var string */
private $method;
+ /** @var string|null */
+ private $inputString;
+
/** @var bool */
private $doIncludeStderr = false;
@@ -185,6 +188,18 @@
*/
public function profileMethod( $method ) {
$this->method = $method;
+
+ return $this;
+ }
+
+ /**
+ * Sends the provided input to the command.
+ * When set to null (default), the command will use the standard input.
+ * @param string|null $inputString
+ * @return $this
+ */
+ public function input( $inputString ) {
+ $this->inputString = is_null( $inputString ) ? null :
(string)$inputString;
return $this;
}
@@ -350,7 +365,7 @@
}
$desc = [
- 0 => [ 'file', 'php://stdin', 'r' ],
+ 0 => $this->inputString === null ? [ 'file',
'php://stdin', 'r' ] : [ 'pipe', 'r' ],
1 => [ 'pipe', 'w' ],
2 => [ 'pipe', 'w' ],
];
@@ -364,8 +379,13 @@
$this->logger->error( "proc_open() failed: {command}",
[ 'command' => $cmd ] );
throw new ProcOpenError();
}
- $outBuffer = $logBuffer = '';
- $errBuffer = null;
+
+ $buffers = [
+ 0 => $this->inputString, // input
+ 1 => '', // stdout
+ 2 => null, // stderr
+ 3 => '', // log
+ ];
$emptyArray = [];
$status = false;
$logMsg = false;
@@ -399,8 +419,6 @@
}
}
- $readyPipes = $pipes;
-
// clear get_last_error without actually raising an
error
// from
http://php.net/manual/en/function.error-get-last.php#113518
// TODO replace with clear_last_error when requirements
are bumped to PHP7
@@ -410,8 +428,17 @@
@trigger_error( '' );
restore_error_handler();
+ $readPipes = wfArrayFilterByKey( $pipes, function ( $fd
) use ( $desc ) {
+ return $desc[$fd][0] === 'pipe' &&
$desc[$fd][1] === 'r';
+ } );
+ $writePipes = wfArrayFilterByKey( $pipes, function (
$fd ) use ( $desc ) {
+ return $desc[$fd][0] === 'pipe' &&
$desc[$fd][1] === 'w';
+ } );
+ // stream_select parameter names are from the POV of us
being able to do the operation;
+ // proc_open desriptor types are from the POV of the
process doing it.
+ // So $writePipes is passed as the $read parameter and
$readPipes as $write.
// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
- $numReadyPipes = @stream_select( $readyPipes,
$emptyArray, $emptyArray, $timeout );
+ $numReadyPipes = @stream_select( $writePipes,
$readPipes, $emptyArray, $timeout );
if ( $numReadyPipes === false ) {
$error = error_get_last();
if ( strncmp( $error['message'], $eintrMessage,
strlen( $eintrMessage ) ) == 0 ) {
@@ -422,31 +449,36 @@
break;
}
}
- foreach ( $readyPipes as $fd => $pipe ) {
- $block = fread( $pipe, 65536 );
- if ( $block === '' ) {
+ foreach ( $writePipes + $readPipes as $fd => $pipe ) {
+ // True if a pipe is unblocked for us to write
into, false if for reading from
+ $isWrite = array_key_exists( $fd, $readPipes );
+
+ if ( $isWrite ) {
+ $res = fwrite( $pipe, $buffers[$fd],
65536 );
+ } else {
+ $res = fread( $pipe, 65536 );
+ }
+
+ if ( $res === false ) {
+ $logMsg = 'Error ' . ( $isWrite ?
'writing to' : 'reading from' ) . ' pipe';
+ break 2;
+ }
+
+ if ( $res === '' || $res === 0 ) {
// End of file
fclose( $pipes[$fd] );
unset( $pipes[$fd] );
if ( !$pipes ) {
break 2;
}
- } elseif ( $block === false ) {
- // Read error
- $logMsg = "Error reading from pipe";
- break 2;
- } elseif ( $fd == 1 ) {
- // From stdout
- $outBuffer .= $block;
- } elseif ( $fd == 2 ) {
- // From stderr
- $errBuffer .= $block;
- } elseif ( $fd == 3 ) {
- // From log FD
- $logBuffer .= $block;
- if ( strpos( $block, "\n" ) !== false )
{
- $lines = explode( "\n",
$logBuffer );
- $logBuffer = array_pop( $lines
);
+ } elseif ( $isWrite ) {
+ $buffers[$fd] = substr( $buffers[$fd],
$res );
+ } else {
+ $buffers[$fd] .= $res;
+ if ( $fd === 3 && strpos( $res, "\n" )
!== false ) {
+ // For the log FD, every line
is a separate log entry.
+ $lines = explode( "\n",
$buffers[3] );
+ $buffers[3] = array_pop( $lines
);
foreach ( $lines as $line ) {
$this->logger->info(
$line );
}
@@ -491,15 +523,15 @@
$this->logger->warning( "$logMsg: {command}", [
'command' => $cmd ] );
}
- if ( $errBuffer && $this->doLogStderr ) {
+ if ( $buffers[2] && $this->doLogStderr ) {
$this->logger->error( "Error running {command}:
{error}", [
'command' => $cmd,
- 'error' => $errBuffer,
+ 'error' => $buffers[2],
'exitcode' => $retval,
'exception' => new Exception( 'Shell error' ),
] );
}
- return new Result( $retval, $outBuffer, $errBuffer );
+ return new Result( $retval, $buffers[1], $buffers[2] );
}
}
diff --git a/includes/shell/Shell.php b/includes/shell/Shell.php
index 05463db..d57bf4f 100644
--- a/includes/shell/Shell.php
+++ b/includes/shell/Shell.php
@@ -31,6 +31,7 @@
*
* Use call chaining with this class for expressiveness:
* $result = Shell::command( 'some command' )
+ * ->input( 'foo' )
* ->environment( [ 'ENVIRONMENT_VARIABLE' => 'VALUE' ] )
* ->limits( [ 'time' => 300 ] )
* ->execute();
diff --git a/tests/phpunit/includes/shell/CommandTest.php
b/tests/phpunit/includes/shell/CommandTest.php
index 2bafa03..2ba7bdc 100644
--- a/tests/phpunit/includes/shell/CommandTest.php
+++ b/tests/phpunit/includes/shell/CommandTest.php
@@ -154,4 +154,21 @@
$this->assertSame( 1, count( $logger->getBuffer() ) );
$this->assertSame( trim( $logger->getBuffer()[0][2]['error'] ),
'ThisIsStderr' );
}
+
+ public function testInput() {
+ $this->requirePosix();
+
+ $command = new Command();
+ $command->params( 'cat' );
+ $command->input( 'abc' );
+ $result = $command->execute();
+ $this->assertSame( 'abc', $result->getStdout() );
+
+ // now try it with something that does not fit into a single
block
+ $command = new Command();
+ $command->params( 'cat' );
+ $command->input( str_repeat( '!', 1000000 ) );
+ $result = $command->execute();
+ $this->assertSame( 1000000, strlen( $result->getStdout() ) );
+ }
}
--
To view, visit https://gerrit.wikimedia.org/r/399768
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Ib68180c7af12558686f4864c24fd85f01201d6fb
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Gergő Tisza <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: Gergő Tisza <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: MaxSem <[email protected]>
Gerrit-Reviewer: Reedy <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits