27 use Psr\Log\LoggerAwareTrait;
28 use Psr\Log\NullLogger;
29 use Wikimedia\AtEase\AtEase;
92 $this->setLogger(
new NullLogger() );
99 if ( !$this->everExecuted ) {
101 $message = __CLASS__ .
" was instantiated, but execute() was never called.";
102 if ( $this->method ) {
103 $message .=
' Calling method: {method}.';
106 $message .=
' Command: {command}';
107 $this->logger->warning( $message,
$context );
119 if ( count(
$args ) === 1 && is_array( reset(
$args ) ) ) {
137 if ( count(
$args ) === 1 && is_array( reset(
$args ) ) ) {
143 function ( $value ) {
144 return $value !==
null;
147 $this->command = trim( $this->command .
' ' . implode(
' ',
$args ) );
160 if ( !isset(
$limits[
'walltime'] ) && isset(
$limits[
'time'] ) ) {
214 $this->doIncludeStderr = $yesno;
226 $this->doLogStderr = $yesno;
264 return ( $this->restrictions & $restriction ) === $restriction;
291 foreach ( $this->env as $k => $v ) {
299 $envcmd .=
"set $k=" . preg_replace(
'/([&|()<>^"])/',
'^\\1', $v ) .
'&& ';
304 $envcmd .=
"$k=" . escapeshellarg( $v ) .
' ';
311 if ( is_executable(
'/bin/bash' ) ) {
312 $time = intval( $this->
limits[
'time'] );
313 $wallTime = intval( $this->
limits[
'walltime'] );
314 $mem = intval( $this->
limits[
'memory'] );
315 $filesize = intval( $this->
limits[
'filesize'] );
317 if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) {
318 $cmd =
'/bin/bash ' . escapeshellarg( __DIR__ .
'/limit.sh' ) .
' ' .
319 escapeshellarg( $cmd ) .
' ' .
321 "MW_INCLUDE_STDERR=" . ( $this->doIncludeStderr ?
'1' :
'' ) .
';' .
322 "MW_CPU_LIMIT=$time; " .
323 'MW_CGROUP=' . escapeshellarg( $this->
cgroup ) .
'; ' .
324 "MW_MEM_LIMIT=$mem; " .
325 "MW_FILE_SIZE_LIMIT=$filesize; " .
326 "MW_WALL_CLOCK_LIMIT=$wallTime; " .
327 "MW_USE_LOG_PIPE=yes"
332 if ( !$useLogPipe && $this->doIncludeStderr ) {
336 return [ $cmd, $useLogPipe ];
349 $this->everExecuted =
true;
355 $this->logger->debug( __METHOD__ .
": $cmd" );
362 throw new Exception( __METHOD__ .
363 '(): total length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' );
367 0 => $this->inputString ===
null ? [
'file',
'php://stdin',
'r' ] : [
'pipe',
'r' ],
368 1 => [
'pipe',
'w' ],
369 2 => [
'pipe',
'w' ],
372 $desc[3] = [
'pipe',
'w' ];
375 $scoped =
Profiler::instance()->scopedProfileIn( __FUNCTION__ .
'-' . $profileMethod );
376 $proc = proc_open( $cmd, $desc, $pipes );
378 $this->logger->error(
"proc_open() failed: {command}", [
'command' => $cmd ] );
403 $eintr = defined(
'SOCKET_EINTR' ) ? SOCKET_EINTR : 4;
404 $eintrMessage =
"stream_select(): unable to select [$eintr]";
411 foreach ( $pipes as $pipe ) {
412 stream_set_blocking( $pipe,
false );
419 while ( $pipes && ( $running ===
true || $numReadyPipes !== 0 ) ) {
421 $status = proc_get_status( $proc );
434 set_error_handler(
function () {
436 AtEase::suppressWarnings();
438 AtEase::restoreWarnings();
439 restore_error_handler();
441 $readPipes = array_filter( $pipes,
function ( $fd ) use ( $desc ) {
442 return $desc[$fd][0] ===
'pipe' && $desc[$fd][1] ===
'r';
443 }, ARRAY_FILTER_USE_KEY );
444 $writePipes = array_filter( $pipes,
function ( $fd ) use ( $desc ) {
445 return $desc[$fd][0] ===
'pipe' && $desc[$fd][1] ===
'w';
446 }, ARRAY_FILTER_USE_KEY );
450 AtEase::suppressWarnings();
451 $numReadyPipes = stream_select( $writePipes, $readPipes, $emptyArray, $timeout );
452 AtEase::restoreWarnings();
453 if ( $numReadyPipes ===
false ) {
454 $error = error_get_last();
455 if ( strncmp( $error[
'message'], $eintrMessage, strlen( $eintrMessage ) ) == 0 ) {
458 trigger_error( $error[
'message'], E_USER_WARNING );
459 $logMsg = $error[
'message'];
463 foreach ( $writePipes + $readPipes as $fd => $pipe ) {
465 $isWrite = array_key_exists( $fd, $readPipes );
469 if ( $buffers[$fd] ===
'' ) {
470 fclose( $pipes[$fd] );
471 unset( $pipes[$fd] );
474 $res = fwrite( $pipe, $buffers[$fd], 65536 );
476 $res = fread( $pipe, 65536 );
479 if (
$res ===
false ) {
480 $logMsg =
'Error ' . ( $isWrite ?
'writing to' :
'reading from' ) .
' pipe';
486 if ( feof( $pipe ) ) {
487 fclose( $pipes[$fd] );
488 unset( $pipes[$fd] );
490 } elseif ( $isWrite ) {
491 $buffers[$fd] = (string)substr( $buffers[$fd],
$res );
492 if ( $buffers[$fd] ===
'' ) {
493 fclose( $pipes[$fd] );
494 unset( $pipes[$fd] );
497 $buffers[$fd] .=
$res;
498 if ( $fd === 3 && strpos(
$res,
"\n" ) !==
false ) {
500 $lines = explode(
"\n", $buffers[3] );
501 $buffers[3] = array_pop(
$lines );
503 $this->logger->info(
$line );
510 foreach ( $pipes as $pipe ) {
517 $status = proc_get_status( $proc );
520 if ( $logMsg !==
false ) {
524 } elseif (
$status[
'signaled'] ) {
525 $logMsg =
"Exited with signal {$status['termsig']}";
526 $retval = 128 +
$status[
'termsig'];
530 $retval = proc_close( $proc );
535 if ( $retval == 127 ) {
536 $logMsg =
"Possibly missing executable file";
537 } elseif ( $retval >= 129 && $retval <= 192 ) {
538 $logMsg =
"Probably exited with signal " . ( $retval - 128 );
542 if ( $logMsg !==
false ) {
543 $this->logger->warning(
"$logMsg: {command}", [
'command' => $cmd ] );
546 if ( $buffers[2] && $this->doLogStderr ) {
547 $this->logger->error(
"Error running {command}: {error}", [
549 'error' => $buffers[2],
550 'exitcode' => $retval,
551 'exception' =>
new Exception(
'Shell error' ),
555 return new Result( $retval, $buffers[1], $buffers[2] );
566 return "#Command: {$this->command}";