Skip to content

Commit

Permalink
Allow programmatic input in Command
Browse files Browse the repository at this point in the history
Bug: T182463
Change-Id: Ib68180c7af12558686f4864c24fd85f01201d6fb
  • Loading branch information
tgr authored and MaxSem committed Jan 3, 2018
1 parent 7fa2c94 commit 0e211c4
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 28 deletions.
88 changes: 60 additions & 28 deletions includes/shell/Command.php
Expand Up @@ -56,6 +56,9 @@ class Command {
/** @var string */
private $method;

/** @var string|null */
private $inputString;

/** @var bool */
private $doIncludeStderr = false;

Expand Down Expand Up @@ -189,6 +192,18 @@ public function profileMethod( $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;
}

/**
* Controls whether stderr should be included in stdout, including errors from limit.sh.
* Default: don't include.
Expand Down Expand Up @@ -350,7 +365,7 @@ public function execute() {
}

$desc = [
0 => [ 'file', 'php://stdin', 'r' ],
0 => $this->inputString === null ? [ 'file', 'php://stdin', 'r' ] : [ 'pipe', 'r' ],
1 => [ 'pipe', 'w' ],
2 => [ 'pipe', 'w' ],
];
Expand All @@ -364,8 +379,13 @@ public function execute() {
$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;
Expand Down Expand Up @@ -399,8 +419,6 @@ public function execute() {
}
}

$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
Expand All @@ -410,8 +428,17 @@ public function execute() {
@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 ) {
Expand All @@ -422,31 +449,36 @@ public function execute() {
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 );
}
Expand Down Expand Up @@ -491,15 +523,15 @@ public function execute() {
$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] );
}
}
1 change: 1 addition & 0 deletions includes/shell/Shell.php
Expand Up @@ -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();
Expand Down
17 changes: 17 additions & 0 deletions tests/phpunit/includes/shell/CommandTest.php
Expand Up @@ -154,4 +154,21 @@ public function testLogStderr() {
$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() ) );
}
}

0 comments on commit 0e211c4

Please sign in to comment.