306 foreach ( $this->env as $k => $v ) {
314 $envcmd .=
"set $k=" . preg_replace(
'/([&|()<>^"])/',
'^\\1', $v ) .
'&& ';
319 $envcmd .=
"$k=" . escapeshellarg( $v ) .
' ';
326 if ( is_executable(
'/bin/bash' ) ) {
327 $time = intval( $this->limits[
'time'] );
328 $wallTime = intval( $this->limits[
'walltime'] );
329 $mem = intval( $this->limits[
'memory'] );
330 $filesize = intval( $this->limits[
'filesize'] );
332 if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) {
333 $cmd =
'/bin/bash ' . escapeshellarg( __DIR__ .
'/limit.sh' ) .
' ' .
334 escapeshellarg( $cmd ) .
' ' .
336 "MW_INCLUDE_STDERR=" . ( $this->doIncludeStderr ?
'1' :
'' ) .
';' .
337 "MW_CPU_LIMIT=$time; " .
338 'MW_CGROUP=' . escapeshellarg( $this->cgroup ) .
'; ' .
339 "MW_MEM_LIMIT=$mem; " .
340 "MW_FILE_SIZE_LIMIT=$filesize; " .
341 "MW_WALL_CLOCK_LIMIT=$wallTime; " .
342 "MW_USE_LOG_PIPE=yes"
347 if ( !$useLogPipe && $this->doIncludeStderr ) {
352 $cmd =
'cmd.exe /c "' . $cmd .
'"';
355 return [ $cmd, $useLogPipe ];
368 $this->everExecuted =
true;
372 list( $cmd, $useLogPipe ) = $this->buildFinalCommand( $this->command );
374 $this->logger->debug( __METHOD__ .
": $cmd" );
381 throw new Exception( __METHOD__ .
382 '(): total length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' );
386 0 => $this->inputString ===
null ? [
'file',
'php://stdin',
'r' ] : [
'pipe',
'r' ],
387 1 => [
'pipe',
'w' ],
388 2 => [
'pipe',
'w' ],
391 $desc[3] = [
'pipe',
'w' ];
394 $scoped = Profiler::instance()->scopedProfileIn( __FUNCTION__ .
'-' . $profileMethod );
400 $proc = proc_open( $cmd, $desc, $pipes,
null,
null, [
'bypass_shell' =>
true ] );
402 $proc = proc_open( $cmd, $desc, $pipes );
406 $this->logger->error(
"proc_open() failed: {command}", [
'command' => $cmd ] );
411 0 => $this->inputString,
431 $eintr = defined(
'SOCKET_EINTR' ) ? SOCKET_EINTR : 4;
432 $eintrMessage =
"stream_select(): unable to select [$eintr]";
439 foreach ( $pipes as $pipe ) {
440 stream_set_blocking( $pipe,
false );
447 while ( $pipes && ( $running ===
true || $numReadyPipes !== 0 ) ) {
449 $status = proc_get_status( $proc );
452 if ( !$status[
'running'] ) {
462 set_error_handler(
function () {
464 AtEase::suppressWarnings();
466 AtEase::restoreWarnings();
467 restore_error_handler();
469 $readPipes = array_filter( $pipes,
function ( $fd ) use ( $desc ) {
470 return $desc[$fd][0] ===
'pipe' && $desc[$fd][1] ===
'r';
471 }, ARRAY_FILTER_USE_KEY );
472 $writePipes = array_filter( $pipes,
function ( $fd ) use ( $desc ) {
473 return $desc[$fd][0] ===
'pipe' && $desc[$fd][1] ===
'w';
474 }, ARRAY_FILTER_USE_KEY );
478 AtEase::suppressWarnings();
479 $numReadyPipes = stream_select( $writePipes, $readPipes, $emptyArray, $timeout );
480 AtEase::restoreWarnings();
481 if ( $numReadyPipes ===
false ) {
482 $error = error_get_last();
483 if ( strncmp( $error[
'message'], $eintrMessage, strlen( $eintrMessage ) ) == 0 ) {
486 trigger_error( $error[
'message'], E_USER_WARNING );
487 $logMsg = $error[
'message'];
491 foreach ( $writePipes + $readPipes as $fd => $pipe ) {
493 $isWrite = array_key_exists( $fd, $readPipes );
497 if ( $buffers[$fd] ===
'' ) {
498 fclose( $pipes[$fd] );
499 unset( $pipes[$fd] );
502 $res = fwrite( $pipe, $buffers[$fd], 65536 );
504 $res = fread( $pipe, 65536 );
507 if (
$res ===
false ) {
508 $logMsg =
'Error ' . ( $isWrite ?
'writing to' :
'reading from' ) .
' pipe';
514 if ( feof( $pipe ) ) {
515 fclose( $pipes[$fd] );
516 unset( $pipes[$fd] );
518 } elseif ( $isWrite ) {
519 $buffers[$fd] = (string)substr( $buffers[$fd],
$res );
520 if ( $buffers[$fd] ===
'' ) {
521 fclose( $pipes[$fd] );
522 unset( $pipes[$fd] );
525 $buffers[$fd] .=
$res;
526 if ( $fd === 3 && strpos(
$res,
"\n" ) !==
false ) {
528 $lines = explode(
"\n", $buffers[3] );
529 $buffers[3] = array_pop(
$lines );
531 $this->logger->info(
$line );
538 foreach ( $pipes as $pipe ) {
545 $status = proc_get_status( $proc );
548 if ( $logMsg !==
false ) {
552 } elseif ( $status[
'signaled'] ) {
553 $logMsg =
"Exited with signal {$status['termsig']}";
554 $retval = 128 + $status[
'termsig'];
557 if ( $status[
'running'] ) {
558 $retval = proc_close( $proc );
560 $retval = $status[
'exitcode'];
563 if ( $retval == 127 ) {
564 $logMsg =
"Possibly missing executable file";
565 } elseif ( $retval >= 129 && $retval <= 192 ) {
566 $logMsg =
"Probably exited with signal " . ( $retval - 128 );
570 if ( $logMsg !==
false ) {
571 $this->logger->warning(
"$logMsg: {command}", [
'command' => $cmd ] );
574 if ( $buffers[2] && $this->doLogStderr ) {
575 $this->logger->error(
"Error running {command}: {error}", [
577 'error' => $buffers[2],
578 'exitcode' => $retval,
579 'exception' =>
new Exception(
'Shell error' ),
583 return new Result( $retval, $buffers[1], $buffers[2] );