$functions = explode( ',', ini_get( 'disable_functions' ) );
$functions = array_map( 'trim', $functions );
$functions = array_map( 'strtolower', $functions );
- if ( in_array( 'passthru', $functions ) ) {
- wfDebug( "passthru is in disabled_functions\n" );
- $disabled = 'passthru';
+ if ( in_array( 'proc_open', $functions ) ) {
+ wfDebug( "proc_open is in disabled_functions\n" );
+ $disabled = 'disabled';
}
}
}
* configuration if supported.
* @param string $cmd Command line, properly escaped for shell.
* @param &$retval null|Mixed optional, will receive the program's exit code.
- * (non-zero is usually failure)
+ * (non-zero is usually failure). If there is an error from
+ * read, select, or proc_open(), this will be set to -1.
* @param array $environ optional environment variables which should be
* added to the executed command environment.
* @param array $limits optional array with limits(filesize, memory, time, walltime)
* this overwrites the global wgShellMax* limits.
- * @param array $options Array of options. Only one is "duplicateStderr" => true, which
- * Which duplicates stderr to stdout, including errors from limit.sh
+ * @param array $options Array of options:
+ * - duplicateStderr: Set this to true to duplicate stderr to stdout,
+ * including errors from limit.sh
+ *
* @return string collected stdout as a string
*/
function wfShellExec( $cmd, &$retval = null, $environ = array(), $limits = array(), $options = array() ) {
$retval = 1;
return $disabled == 'safemode' ?
'Unable to run external programs in safe mode.' :
- 'Unable to run external programs, passthru() is disabled.';
+ 'Unable to run external programs, proc_open() is disabled.';
}
$includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr'];
}
$cmd = $envcmd . $cmd;
+ $useLogPipe = false;
if ( php_uname( 's' ) == 'Linux' ) {
$time = intval ( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime );
if ( isset( $limits['walltime'] ) ) {
'MW_CGROUP=' . escapeshellarg( $wgShellCgroup ) . '; ' .
"MW_MEM_LIMIT=$mem; " .
"MW_FILE_SIZE_LIMIT=$filesize; " .
- "MW_WALL_CLOCK_LIMIT=$wallTime"
+ "MW_WALL_CLOCK_LIMIT=$wallTime; " .
+ "MW_USE_LOG_PIPE=yes"
);
+ $useLogPipe = true;
} elseif ( $includeStderr ) {
$cmd .= ' 2>&1';
}
}
wfDebug( "wfShellExec: $cmd\n" );
- // Default to an unusual value that shouldn't happen naturally,
- // so in the unlikely event of a weird php bug, it would be
- // more obvious what happened.
- $retval = 200;
- ob_start();
- passthru( $cmd, $retval );
- $output = ob_get_contents();
- ob_end_clean();
+ $desc = array(
+ 0 => array( 'file', 'php://stdin', 'r' ),
+ 1 => array( 'pipe', 'w' ),
+ 2 => array( 'file', 'php://stderr', 'w' ) );
+ if ( $useLogPipe ) {
+ $desc[3] = array( 'pipe', 'w' );
+ }
+ $pipes = null;
+ $proc = proc_open( $cmd, $desc, $pipes );
+ if ( !$proc ) {
+ wfDebugLog( 'exec', "proc_open() failed: $cmd\n" );
+ $retval = -1;
+ return '';
+ }
+ $outBuffer = $logBuffer = '';
+ $emptyArray = array();
+ $status = false;
+ $logMsg = false;
+
+ // According to the documentation, it is possible for stream_select()
+ // to fail due to EINTR. I haven't managed to induce this in testing
+ // despite sending various signals. If it did happen, the error
+ // message would take the form:
+ //
+ // stream_select(): unable to select [4]: Interrupted system call (max_fd=5)
+ //
+ // where [4] is the value of the macro EINTR and "Interrupted system
+ // call" is string which according to the Linux manual is "possibly"
+ // localised according to LC_MESSAGES.
+ $eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4;
+ $eintrMessage = "stream_select(): unable to select [$eintr]";
+
+ // Build a table mapping resource IDs to pipe FDs to work around a
+ // PHP 5.3 issue in which stream_select() does not preserve array keys
+ // <https://bugs.php.net/bug.php?id=53427>.
+ $fds = array();
+ foreach ( $pipes as $fd => $pipe ) {
+ $fds[(int)$pipe] = $fd;
+ }
+
+ while ( true ) {
+ $status = proc_get_status( $proc );
+ if ( !$status['running'] ) {
+ break;
+ }
+ $status = false;
+
+ $readyPipes = $pipes;
- if ( $retval == 127 ) {
- wfDebugLog( 'exec', "Possibly missing executable file: $cmd\n" );
+ // Clear last error
+ @trigger_error( '' );
+ if ( @stream_select( $readyPipes, $emptyArray, $emptyArray, null ) === false ) {
+ $error = error_get_last();
+ if ( strncmp( $error['message'], $eintrMessage, strlen( $eintrMessage ) ) == 0 ) {
+ continue;
+ } else {
+ trigger_error( $error['message'], E_USER_WARNING );
+ $logMsg = $error['message'];
+ break;
+ }
+ }
+ foreach ( $readyPipes as $pipe ) {
+ $block = fread( $pipe, 65536 );
+ $fd = $fds[(int)$pipe];
+ if ( $block === '' ) {
+ // 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 == 3 ) {
+ // From log FD
+ $logBuffer .= $block;
+ if ( strpos( $block, "\n" ) !== false ) {
+ $lines = explode( "\n", $logBuffer );
+ $logBuffer = array_pop( $lines );
+ foreach ( $lines as $line ) {
+ wfDebugLog( 'exec', $line );
+ }
+ }
+ }
+ }
}
- return $output;
+
+ foreach ( $pipes as $pipe ) {
+ fclose( $pipe );
+ }
+
+ // Use the status previously collected if possible, since proc_get_status()
+ // just calls waitpid() which will not return anything useful the second time.
+ if ( $status === false ) {
+ $status = proc_get_status( $proc );
+ }
+
+ if ( $logMsg !== false ) {
+ // Read/select error
+ $retval = -1;
+ proc_close( $proc );
+ } elseif ( $status['signaled'] ) {
+ $logMsg = "Exited with signal {$status['termsig']}";
+ $retval = 128 + $status['termsig'];
+ proc_close( $proc );
+ } else {
+ if ( $status['running'] ) {
+ $retval = proc_close( $proc );
+ } else {
+ $retval = $status['exitcode'];
+ proc_close( $proc );
+ }
+ if ( $retval == 127 ) {
+ $logMsg = "Possibly missing executable file";
+ } elseif ( $retval >= 129 && $retval <= 192 ) {
+ $logMsg = "Probably exited with signal " . ( $retval - 128 );
+ }
+ }
+
+ if ( $logMsg !== false ) {
+ wfDebugLog( 'exec', "$logMsg: $cmd\n" );
+ }
+
+ return $outBuffer;
}
/**
* This is useful for extensions which due to their nature are not kept in sync
* with releases
*
+ * Note: Due to the behavior of PHP's version_compare() which is used in this
+ * fuction, if you want to allow the 'wmf' development versions add a 'c' (or
+ * any single letter other than 'a', 'b' or 'p') as a post-fix to your
+ * targeted version number. For example if you wanted to allow any variation
+ * of 1.22 use `wfUseMW( '1.22c' )`. Using an 'a' or 'b' instead of 'c' will
+ * not result in the same comparison due to the internal logic of
+ * version_compare().
+ *
* @see perldoc -f use
*
* @param $req_ver Mixed: the version to check, can be a string, an integer, or