1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Process\Tests; 13 14use PHPUnit\Framework\TestCase; 15use Symfony\Component\Process\Exception\LogicException; 16use Symfony\Component\Process\Exception\ProcessTimedOutException; 17use Symfony\Component\Process\Exception\RuntimeException; 18use Symfony\Component\Process\InputStream; 19use Symfony\Component\Process\PhpExecutableFinder; 20use Symfony\Component\Process\Pipes\PipesInterface; 21use Symfony\Component\Process\Process; 22 23/** 24 * @author Robert Schönthal <seroscho@googlemail.com> 25 */ 26class ProcessTest extends TestCase 27{ 28 private static $phpBin; 29 private static $process; 30 private static $sigchild; 31 private static $notEnhancedSigchild = false; 32 33 public static function setUpBeforeClass() 34 { 35 $phpBin = new PhpExecutableFinder(); 36 self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === \PHP_SAPI ? 'php' : $phpBin->find()); 37 38 ob_start(); 39 phpinfo(\INFO_GENERAL); 40 self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); 41 } 42 43 protected function tearDown() 44 { 45 if (self::$process) { 46 self::$process->stop(0); 47 self::$process = null; 48 } 49 } 50 51 /** 52 * @group legacy 53 * @expectedDeprecation The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0. 54 */ 55 public function testInvalidCwd() 56 { 57 if ('\\' === \DIRECTORY_SEPARATOR) { 58 $this->markTestSkipped('False-positive on Windows/appveyor.'); 59 } 60 61 // Check that it works fine if the CWD exists 62 $cmd = new Process('echo test', __DIR__); 63 $cmd->run(); 64 65 $cmd = new Process('echo test', __DIR__.'/notfound/'); 66 $cmd->run(); 67 } 68 69 public function testThatProcessDoesNotThrowWarningDuringRun() 70 { 71 if ('\\' === \DIRECTORY_SEPARATOR) { 72 $this->markTestSkipped('This test is transient on Windows'); 73 } 74 @trigger_error('Test Error', \E_USER_NOTICE); 75 $process = $this->getProcessForCode('sleep(3)'); 76 $process->run(); 77 $actualError = error_get_last(); 78 $this->assertEquals('Test Error', $actualError['message']); 79 $this->assertEquals(\E_USER_NOTICE, $actualError['type']); 80 } 81 82 public function testNegativeTimeoutFromConstructor() 83 { 84 $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); 85 $this->getProcess('', null, null, null, -1); 86 } 87 88 public function testNegativeTimeoutFromSetter() 89 { 90 $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); 91 $p = $this->getProcess(''); 92 $p->setTimeout(-1); 93 } 94 95 public function testFloatAndNullTimeout() 96 { 97 $p = $this->getProcess(''); 98 99 $p->setTimeout(10); 100 $this->assertSame(10.0, $p->getTimeout()); 101 102 $p->setTimeout(null); 103 $this->assertNull($p->getTimeout()); 104 105 $p->setTimeout(0.0); 106 $this->assertNull($p->getTimeout()); 107 } 108 109 /** 110 * @requires extension pcntl 111 */ 112 public function testStopWithTimeoutIsActuallyWorking() 113 { 114 $p = $this->getProcess([self::$phpBin, __DIR__.'/NonStopableProcess.php', 30]); 115 $p->start(); 116 117 while (false === strpos($p->getOutput(), 'received')) { 118 usleep(1000); 119 } 120 $start = microtime(true); 121 $p->stop(0.1); 122 123 $p->wait(); 124 125 $this->assertLessThan(15, microtime(true) - $start); 126 } 127 128 public function testAllOutputIsActuallyReadOnTermination() 129 { 130 // this code will result in a maximum of 2 reads of 8192 bytes by calling 131 // start() and isRunning(). by the time getOutput() is called the process 132 // has terminated so the internal pipes array is already empty. normally 133 // the call to start() will not read any data as the process will not have 134 // generated output, but this is non-deterministic so we must count it as 135 // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus 136 // another byte which will never be read. 137 $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2; 138 139 $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); 140 $p = $this->getProcessForCode($code); 141 142 $p->start(); 143 144 // Don't call Process::run nor Process::wait to avoid any read of pipes 145 $h = new \ReflectionProperty($p, 'process'); 146 $h->setAccessible(true); 147 $h = $h->getValue($p); 148 $s = @proc_get_status($h); 149 150 while (!empty($s['running'])) { 151 usleep(1000); 152 $s = proc_get_status($h); 153 } 154 155 $o = $p->getOutput(); 156 157 $this->assertEquals($expectedOutputSize, \strlen($o)); 158 } 159 160 public function testCallbacksAreExecutedWithStart() 161 { 162 $process = $this->getProcess('echo foo'); 163 $process->start(function ($type, $buffer) use (&$data) { 164 $data .= $buffer; 165 }); 166 167 $process->wait(); 168 169 $this->assertSame('foo'.\PHP_EOL, $data); 170 } 171 172 /** 173 * tests results from sub processes. 174 * 175 * @dataProvider responsesCodeProvider 176 */ 177 public function testProcessResponses($expected, $getter, $code) 178 { 179 $p = $this->getProcessForCode($code); 180 $p->run(); 181 182 $this->assertSame($expected, $p->$getter()); 183 } 184 185 /** 186 * tests results from sub processes. 187 * 188 * @dataProvider pipesCodeProvider 189 */ 190 public function testProcessPipes($code, $size) 191 { 192 $expected = str_repeat(str_repeat('*', 1024), $size).'!'; 193 $expectedLength = (1024 * $size) + 1; 194 195 $p = $this->getProcessForCode($code); 196 $p->setInput($expected); 197 $p->run(); 198 199 $this->assertEquals($expectedLength, \strlen($p->getOutput())); 200 $this->assertEquals($expectedLength, \strlen($p->getErrorOutput())); 201 } 202 203 /** 204 * @dataProvider pipesCodeProvider 205 */ 206 public function testSetStreamAsInput($code, $size) 207 { 208 $expected = str_repeat(str_repeat('*', 1024), $size).'!'; 209 $expectedLength = (1024 * $size) + 1; 210 211 $stream = fopen('php://temporary', 'w+'); 212 fwrite($stream, $expected); 213 rewind($stream); 214 215 $p = $this->getProcessForCode($code); 216 $p->setInput($stream); 217 $p->run(); 218 219 fclose($stream); 220 221 $this->assertEquals($expectedLength, \strlen($p->getOutput())); 222 $this->assertEquals($expectedLength, \strlen($p->getErrorOutput())); 223 } 224 225 public function testLiveStreamAsInput() 226 { 227 $stream = fopen('php://memory', 'r+'); 228 fwrite($stream, 'hello'); 229 rewind($stream); 230 231 $p = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); 232 $p->setInput($stream); 233 $p->start(function ($type, $data) use ($stream) { 234 if ('hello' === $data) { 235 fclose($stream); 236 } 237 }); 238 $p->wait(); 239 240 $this->assertSame('hello', $p->getOutput()); 241 } 242 243 public function testSetInputWhileRunningThrowsAnException() 244 { 245 $this->expectException('Symfony\Component\Process\Exception\LogicException'); 246 $this->expectExceptionMessage('Input can not be set while the process is running.'); 247 $process = $this->getProcessForCode('sleep(30);'); 248 $process->start(); 249 try { 250 $process->setInput('foobar'); 251 $process->stop(); 252 $this->fail('A LogicException should have been raised.'); 253 } catch (LogicException $e) { 254 } 255 $process->stop(); 256 257 throw $e; 258 } 259 260 /** 261 * @dataProvider provideInvalidInputValues 262 */ 263 public function testInvalidInput($value) 264 { 265 $this->expectException('Symfony\Component\Process\Exception\InvalidArgumentException'); 266 $this->expectExceptionMessage('"Symfony\Component\Process\Process::setInput" only accepts strings, Traversable objects or stream resources.'); 267 $process = $this->getProcess('foo'); 268 $process->setInput($value); 269 } 270 271 public function provideInvalidInputValues() 272 { 273 return [ 274 [[]], 275 [new NonStringifiable()], 276 ]; 277 } 278 279 /** 280 * @dataProvider provideInputValues 281 */ 282 public function testValidInput($expected, $value) 283 { 284 $process = $this->getProcess('foo'); 285 $process->setInput($value); 286 $this->assertSame($expected, $process->getInput()); 287 } 288 289 public function provideInputValues() 290 { 291 return [ 292 [null, null], 293 ['24.5', 24.5], 294 ['input data', 'input data'], 295 ]; 296 } 297 298 public function chainedCommandsOutputProvider() 299 { 300 if ('\\' === \DIRECTORY_SEPARATOR) { 301 return [ 302 ["2 \r\n2\r\n", '&&', '2'], 303 ]; 304 } 305 306 return [ 307 ["1\n1\n", ';', '1'], 308 ["2\n2\n", '&&', '2'], 309 ]; 310 } 311 312 /** 313 * @dataProvider chainedCommandsOutputProvider 314 */ 315 public function testChainedCommandsOutput($expected, $operator, $input) 316 { 317 $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input)); 318 $process->run(); 319 $this->assertEquals($expected, $process->getOutput()); 320 } 321 322 public function testCallbackIsExecutedForOutput() 323 { 324 $p = $this->getProcessForCode('echo \'foo\';'); 325 326 $called = false; 327 $p->run(function ($type, $buffer) use (&$called) { 328 $called = 'foo' === $buffer; 329 }); 330 331 $this->assertTrue($called, 'The callback should be executed with the output'); 332 } 333 334 public function testCallbackIsExecutedForOutputWheneverOutputIsDisabled() 335 { 336 $p = $this->getProcessForCode('echo \'foo\';'); 337 $p->disableOutput(); 338 339 $called = false; 340 $p->run(function ($type, $buffer) use (&$called) { 341 $called = 'foo' === $buffer; 342 }); 343 344 $this->assertTrue($called, 'The callback should be executed with the output'); 345 } 346 347 public function testGetErrorOutput() 348 { 349 $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'); 350 351 $p->run(); 352 $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches)); 353 } 354 355 public function testFlushErrorOutput() 356 { 357 $p = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'); 358 359 $p->run(); 360 $p->clearErrorOutput(); 361 $this->assertEmpty($p->getErrorOutput()); 362 } 363 364 /** 365 * @dataProvider provideIncrementalOutput 366 */ 367 public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri) 368 { 369 $lock = tempnam(sys_get_temp_dir(), __FUNCTION__); 370 371 $p = $this->getProcessForCode('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');'); 372 373 $h = fopen($lock, 'w'); 374 flock($h, \LOCK_EX); 375 376 $p->start(); 377 378 foreach (['foo', 'bar'] as $s) { 379 while (false === strpos($p->$getOutput(), $s)) { 380 usleep(1000); 381 } 382 383 $this->assertSame($s, $p->$getIncrementalOutput()); 384 $this->assertSame('', $p->$getIncrementalOutput()); 385 386 flock($h, \LOCK_UN); 387 } 388 389 fclose($h); 390 } 391 392 public function provideIncrementalOutput() 393 { 394 return [ 395 ['getOutput', 'getIncrementalOutput', 'php://stdout'], 396 ['getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'], 397 ]; 398 } 399 400 public function testGetOutput() 401 { 402 $p = $this->getProcessForCode('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }'); 403 404 $p->run(); 405 $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches)); 406 } 407 408 public function testFlushOutput() 409 { 410 $p = $this->getProcessForCode('$n=0;while ($n<3) {echo \' foo \';$n++;}'); 411 412 $p->run(); 413 $p->clearOutput(); 414 $this->assertEmpty($p->getOutput()); 415 } 416 417 public function testZeroAsOutput() 418 { 419 if ('\\' === \DIRECTORY_SEPARATOR) { 420 // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line 421 $p = $this->getProcess('echo | set /p dummyName=0'); 422 } else { 423 $p = $this->getProcess('printf 0'); 424 } 425 426 $p->run(); 427 $this->assertSame('0', $p->getOutput()); 428 } 429 430 public function testExitCodeCommandFailed() 431 { 432 if ('\\' === \DIRECTORY_SEPARATOR) { 433 $this->markTestSkipped('Windows does not support POSIX exit code'); 434 } 435 $this->skipIfNotEnhancedSigchild(); 436 437 // such command run in bash return an exitcode 127 438 $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis'); 439 $process->run(); 440 441 $this->assertGreaterThan(0, $process->getExitCode()); 442 } 443 444 public function testTTYCommand() 445 { 446 if ('\\' === \DIRECTORY_SEPARATOR) { 447 $this->markTestSkipped('Windows does not have /dev/tty support'); 448 } 449 450 $process = $this->getProcess('echo "foo" >> /dev/null && '.$this->getProcessForCode('usleep(100000);')->getCommandLine()); 451 $process->setTty(true); 452 $process->start(); 453 $this->assertTrue($process->isRunning()); 454 $process->wait(); 455 456 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); 457 } 458 459 public function testTTYCommandExitCode() 460 { 461 if ('\\' === \DIRECTORY_SEPARATOR) { 462 $this->markTestSkipped('Windows does have /dev/tty support'); 463 } 464 $this->skipIfNotEnhancedSigchild(); 465 466 $process = $this->getProcess('echo "foo" >> /dev/null'); 467 $process->setTty(true); 468 $process->run(); 469 470 $this->assertTrue($process->isSuccessful()); 471 } 472 473 public function testTTYInWindowsEnvironment() 474 { 475 $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); 476 $this->expectExceptionMessage('TTY mode is not supported on Windows platform.'); 477 if ('\\' !== \DIRECTORY_SEPARATOR) { 478 $this->markTestSkipped('This test is for Windows platform only'); 479 } 480 481 $process = $this->getProcess('echo "foo" >> /dev/null'); 482 $process->setTty(false); 483 $process->setTty(true); 484 } 485 486 public function testExitCodeTextIsNullWhenExitCodeIsNull() 487 { 488 $this->skipIfNotEnhancedSigchild(); 489 490 $process = $this->getProcess(''); 491 $this->assertNull($process->getExitCodeText()); 492 } 493 494 public function testPTYCommand() 495 { 496 if (!Process::isPtySupported()) { 497 $this->markTestSkipped('PTY is not supported on this operating system.'); 498 } 499 500 $process = $this->getProcess('echo "foo"'); 501 $process->setPty(true); 502 $process->run(); 503 504 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); 505 $this->assertEquals("foo\r\n", $process->getOutput()); 506 } 507 508 public function testMustRun() 509 { 510 $this->skipIfNotEnhancedSigchild(); 511 512 $process = $this->getProcess('echo foo'); 513 514 $this->assertSame($process, $process->mustRun()); 515 $this->assertEquals('foo'.\PHP_EOL, $process->getOutput()); 516 } 517 518 public function testSuccessfulMustRunHasCorrectExitCode() 519 { 520 $this->skipIfNotEnhancedSigchild(); 521 522 $process = $this->getProcess('echo foo')->mustRun(); 523 $this->assertEquals(0, $process->getExitCode()); 524 } 525 526 public function testMustRunThrowsException() 527 { 528 $this->expectException('Symfony\Component\Process\Exception\ProcessFailedException'); 529 $this->skipIfNotEnhancedSigchild(); 530 531 $process = $this->getProcess('exit 1'); 532 $process->mustRun(); 533 } 534 535 public function testExitCodeText() 536 { 537 $this->skipIfNotEnhancedSigchild(); 538 539 $process = $this->getProcess(''); 540 $r = new \ReflectionObject($process); 541 $p = $r->getProperty('exitcode'); 542 $p->setAccessible(true); 543 544 $p->setValue($process, 2); 545 $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText()); 546 } 547 548 public function testStartIsNonBlocking() 549 { 550 $process = $this->getProcessForCode('usleep(500000);'); 551 $start = microtime(true); 552 $process->start(); 553 $end = microtime(true); 554 $this->assertLessThan(0.4, $end - $start); 555 $process->stop(); 556 } 557 558 public function testUpdateStatus() 559 { 560 $process = $this->getProcess('echo foo'); 561 $process->run(); 562 $this->assertGreaterThan(0, \strlen($process->getOutput())); 563 } 564 565 public function testGetExitCodeIsNullOnStart() 566 { 567 $this->skipIfNotEnhancedSigchild(); 568 569 $process = $this->getProcessForCode('usleep(100000);'); 570 $this->assertNull($process->getExitCode()); 571 $process->start(); 572 $this->assertNull($process->getExitCode()); 573 $process->wait(); 574 $this->assertEquals(0, $process->getExitCode()); 575 } 576 577 public function testGetExitCodeIsNullOnWhenStartingAgain() 578 { 579 $this->skipIfNotEnhancedSigchild(); 580 581 $process = $this->getProcessForCode('usleep(100000);'); 582 $process->run(); 583 $this->assertEquals(0, $process->getExitCode()); 584 $process->start(); 585 $this->assertNull($process->getExitCode()); 586 $process->wait(); 587 $this->assertEquals(0, $process->getExitCode()); 588 } 589 590 public function testGetExitCode() 591 { 592 $this->skipIfNotEnhancedSigchild(); 593 594 $process = $this->getProcess('echo foo'); 595 $process->run(); 596 $this->assertSame(0, $process->getExitCode()); 597 } 598 599 public function testStatus() 600 { 601 $process = $this->getProcessForCode('usleep(100000);'); 602 $this->assertFalse($process->isRunning()); 603 $this->assertFalse($process->isStarted()); 604 $this->assertFalse($process->isTerminated()); 605 $this->assertSame(Process::STATUS_READY, $process->getStatus()); 606 $process->start(); 607 $this->assertTrue($process->isRunning()); 608 $this->assertTrue($process->isStarted()); 609 $this->assertFalse($process->isTerminated()); 610 $this->assertSame(Process::STATUS_STARTED, $process->getStatus()); 611 $process->wait(); 612 $this->assertFalse($process->isRunning()); 613 $this->assertTrue($process->isStarted()); 614 $this->assertTrue($process->isTerminated()); 615 $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); 616 } 617 618 public function testStop() 619 { 620 $process = $this->getProcessForCode('sleep(31);'); 621 $process->start(); 622 $this->assertTrue($process->isRunning()); 623 $process->stop(); 624 $this->assertFalse($process->isRunning()); 625 } 626 627 public function testIsSuccessful() 628 { 629 $this->skipIfNotEnhancedSigchild(); 630 631 $process = $this->getProcess('echo foo'); 632 $process->run(); 633 $this->assertTrue($process->isSuccessful()); 634 } 635 636 public function testIsSuccessfulOnlyAfterTerminated() 637 { 638 $this->skipIfNotEnhancedSigchild(); 639 640 $process = $this->getProcessForCode('usleep(100000);'); 641 $process->start(); 642 643 $this->assertFalse($process->isSuccessful()); 644 645 $process->wait(); 646 647 $this->assertTrue($process->isSuccessful()); 648 } 649 650 public function testIsNotSuccessful() 651 { 652 $this->skipIfNotEnhancedSigchild(); 653 654 $process = $this->getProcessForCode('throw new \Exception(\'BOUM\');'); 655 $process->run(); 656 $this->assertFalse($process->isSuccessful()); 657 } 658 659 public function testProcessIsNotSignaled() 660 { 661 if ('\\' === \DIRECTORY_SEPARATOR) { 662 $this->markTestSkipped('Windows does not support POSIX signals'); 663 } 664 $this->skipIfNotEnhancedSigchild(); 665 666 $process = $this->getProcess('echo foo'); 667 $process->run(); 668 $this->assertFalse($process->hasBeenSignaled()); 669 } 670 671 public function testProcessWithoutTermSignal() 672 { 673 if ('\\' === \DIRECTORY_SEPARATOR) { 674 $this->markTestSkipped('Windows does not support POSIX signals'); 675 } 676 $this->skipIfNotEnhancedSigchild(); 677 678 $process = $this->getProcess('echo foo'); 679 $process->run(); 680 $this->assertEquals(0, $process->getTermSignal()); 681 } 682 683 public function testProcessIsSignaledIfStopped() 684 { 685 if ('\\' === \DIRECTORY_SEPARATOR) { 686 $this->markTestSkipped('Windows does not support POSIX signals'); 687 } 688 $this->skipIfNotEnhancedSigchild(); 689 690 $process = $this->getProcessForCode('sleep(32);'); 691 $process->start(); 692 $process->stop(); 693 $this->assertTrue($process->hasBeenSignaled()); 694 $this->assertEquals(15, $process->getTermSignal()); // SIGTERM 695 } 696 697 public function testProcessThrowsExceptionWhenExternallySignaled() 698 { 699 $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); 700 $this->expectExceptionMessage('The process has been signaled'); 701 if (!\function_exists('posix_kill')) { 702 $this->markTestSkipped('Function posix_kill is required.'); 703 } 704 $this->skipIfNotEnhancedSigchild(false); 705 706 $process = $this->getProcessForCode('sleep(32.1);'); 707 $process->start(); 708 posix_kill($process->getPid(), 9); // SIGKILL 709 710 $process->wait(); 711 } 712 713 public function testRestart() 714 { 715 $process1 = $this->getProcessForCode('echo getmypid();'); 716 $process1->run(); 717 $process2 = $process1->restart(); 718 719 $process2->wait(); // wait for output 720 721 // Ensure that both processed finished and the output is numeric 722 $this->assertFalse($process1->isRunning()); 723 $this->assertFalse($process2->isRunning()); 724 $this->assertIsNumeric($process1->getOutput()); 725 $this->assertIsNumeric($process2->getOutput()); 726 727 // Ensure that restart returned a new process by check that the output is different 728 $this->assertNotEquals($process1->getOutput(), $process2->getOutput()); 729 } 730 731 public function testRunProcessWithTimeout() 732 { 733 $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); 734 $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); 735 $process = $this->getProcessForCode('sleep(30);'); 736 $process->setTimeout(0.1); 737 $start = microtime(true); 738 try { 739 $process->run(); 740 $this->fail('A RuntimeException should have been raised'); 741 } catch (RuntimeException $e) { 742 } 743 744 $this->assertLessThan(15, microtime(true) - $start); 745 746 throw $e; 747 } 748 749 public function testIterateOverProcessWithTimeout() 750 { 751 $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); 752 $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); 753 $process = $this->getProcessForCode('sleep(30);'); 754 $process->setTimeout(0.1); 755 $start = microtime(true); 756 try { 757 $process->start(); 758 foreach ($process as $buffer); 759 $this->fail('A RuntimeException should have been raised'); 760 } catch (RuntimeException $e) { 761 } 762 763 $this->assertLessThan(15, microtime(true) - $start); 764 765 throw $e; 766 } 767 768 public function testCheckTimeoutOnNonStartedProcess() 769 { 770 $process = $this->getProcess('echo foo'); 771 $this->assertNull($process->checkTimeout()); 772 } 773 774 public function testCheckTimeoutOnTerminatedProcess() 775 { 776 $process = $this->getProcess('echo foo'); 777 $process->run(); 778 $this->assertNull($process->checkTimeout()); 779 } 780 781 public function testCheckTimeoutOnStartedProcess() 782 { 783 $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); 784 $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); 785 $process = $this->getProcessForCode('sleep(33);'); 786 $process->setTimeout(0.1); 787 788 $process->start(); 789 $start = microtime(true); 790 791 try { 792 while ($process->isRunning()) { 793 $process->checkTimeout(); 794 usleep(100000); 795 } 796 $this->fail('A ProcessTimedOutException should have been raised'); 797 } catch (ProcessTimedOutException $e) { 798 } 799 800 $this->assertLessThan(15, microtime(true) - $start); 801 802 throw $e; 803 } 804 805 public function testIdleTimeout() 806 { 807 $process = $this->getProcessForCode('sleep(34);'); 808 $process->setTimeout(60); 809 $process->setIdleTimeout(0.1); 810 811 try { 812 $process->run(); 813 814 $this->fail('A timeout exception was expected.'); 815 } catch (ProcessTimedOutException $e) { 816 $this->assertTrue($e->isIdleTimeout()); 817 $this->assertFalse($e->isGeneralTimeout()); 818 $this->assertEquals(0.1, $e->getExceededTimeout()); 819 } 820 } 821 822 public function testIdleTimeoutNotExceededWhenOutputIsSent() 823 { 824 $process = $this->getProcessForCode('while (true) {echo \'foo \'; usleep(1000);}'); 825 $process->setTimeout(1); 826 $process->start(); 827 828 while (false === strpos($process->getOutput(), 'foo')) { 829 usleep(1000); 830 } 831 832 $process->setIdleTimeout(0.5); 833 834 try { 835 $process->wait(); 836 $this->fail('A timeout exception was expected.'); 837 } catch (ProcessTimedOutException $e) { 838 $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.'); 839 $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.'); 840 $this->assertEquals(1, $e->getExceededTimeout()); 841 } 842 } 843 844 public function testStartAfterATimeout() 845 { 846 $this->expectException('Symfony\Component\Process\Exception\ProcessTimedOutException'); 847 $this->expectExceptionMessage('exceeded the timeout of 0.1 seconds.'); 848 $process = $this->getProcessForCode('sleep(35);'); 849 $process->setTimeout(0.1); 850 851 try { 852 $process->run(); 853 $this->fail('A ProcessTimedOutException should have been raised.'); 854 } catch (ProcessTimedOutException $e) { 855 } 856 $this->assertFalse($process->isRunning()); 857 $process->start(); 858 $this->assertTrue($process->isRunning()); 859 $process->stop(0); 860 861 throw $e; 862 } 863 864 public function testGetPid() 865 { 866 $process = $this->getProcessForCode('sleep(36);'); 867 $process->start(); 868 $this->assertGreaterThan(0, $process->getPid()); 869 $process->stop(0); 870 } 871 872 public function testGetPidIsNullBeforeStart() 873 { 874 $process = $this->getProcess('foo'); 875 $this->assertNull($process->getPid()); 876 } 877 878 public function testGetPidIsNullAfterRun() 879 { 880 $process = $this->getProcess('echo foo'); 881 $process->run(); 882 $this->assertNull($process->getPid()); 883 } 884 885 /** 886 * @requires extension pcntl 887 */ 888 public function testSignal() 889 { 890 $process = $this->getProcess([self::$phpBin, __DIR__.'/SignalListener.php']); 891 $process->start(); 892 893 while (false === strpos($process->getOutput(), 'Caught')) { 894 usleep(1000); 895 } 896 $process->signal(\SIGUSR1); 897 $process->wait(); 898 899 $this->assertEquals('Caught SIGUSR1', $process->getOutput()); 900 } 901 902 /** 903 * @requires extension pcntl 904 */ 905 public function testExitCodeIsAvailableAfterSignal() 906 { 907 $this->skipIfNotEnhancedSigchild(); 908 909 $process = $this->getProcess('sleep 4'); 910 $process->start(); 911 $process->signal(\SIGKILL); 912 913 while ($process->isRunning()) { 914 usleep(10000); 915 } 916 917 $this->assertFalse($process->isRunning()); 918 $this->assertTrue($process->hasBeenSignaled()); 919 $this->assertFalse($process->isSuccessful()); 920 $this->assertEquals(137, $process->getExitCode()); 921 } 922 923 public function testSignalProcessNotRunning() 924 { 925 $this->expectException('Symfony\Component\Process\Exception\LogicException'); 926 $this->expectExceptionMessage('Can not send signal on a non running process.'); 927 $process = $this->getProcess('foo'); 928 $process->signal(1); // SIGHUP 929 } 930 931 /** 932 * @dataProvider provideMethodsThatNeedARunningProcess 933 */ 934 public function testMethodsThatNeedARunningProcess($method) 935 { 936 $process = $this->getProcess('foo'); 937 938 $this->expectException('Symfony\Component\Process\Exception\LogicException'); 939 $this->expectExceptionMessage(sprintf('Process must be started before calling "%s()".', $method)); 940 941 $process->{$method}(); 942 } 943 944 public function provideMethodsThatNeedARunningProcess() 945 { 946 return [ 947 ['getOutput'], 948 ['getIncrementalOutput'], 949 ['getErrorOutput'], 950 ['getIncrementalErrorOutput'], 951 ['wait'], 952 ]; 953 } 954 955 /** 956 * @dataProvider provideMethodsThatNeedATerminatedProcess 957 */ 958 public function testMethodsThatNeedATerminatedProcess($method) 959 { 960 $this->expectException('Symfony\Component\Process\Exception\LogicException'); 961 $this->expectExceptionMessage('Process must be terminated before calling'); 962 $process = $this->getProcessForCode('sleep(37);'); 963 $process->start(); 964 try { 965 $process->{$method}(); 966 $process->stop(0); 967 $this->fail('A LogicException must have been thrown'); 968 } catch (\Exception $e) { 969 } 970 $process->stop(0); 971 972 throw $e; 973 } 974 975 public function provideMethodsThatNeedATerminatedProcess() 976 { 977 return [ 978 ['hasBeenSignaled'], 979 ['getTermSignal'], 980 ['hasBeenStopped'], 981 ['getStopSignal'], 982 ]; 983 } 984 985 /** 986 * @dataProvider provideWrongSignal 987 */ 988 public function testWrongSignal($signal) 989 { 990 if ('\\' === \DIRECTORY_SEPARATOR) { 991 $this->markTestSkipped('POSIX signals do not work on Windows'); 992 } 993 994 if (\PHP_VERSION_ID < 80000 || \is_int($signal)) { 995 $this->expectException(RuntimeException::class); 996 } else { 997 $this->expectException('TypeError'); 998 } 999 1000 $process = $this->getProcessForCode('sleep(38);'); 1001 $process->start(); 1002 try { 1003 $process->signal($signal); 1004 $this->fail('A RuntimeException must have been thrown'); 1005 } catch (\TypeError $e) { 1006 $process->stop(0); 1007 } catch (RuntimeException $e) { 1008 $process->stop(0); 1009 } 1010 1011 throw $e; 1012 } 1013 1014 public function provideWrongSignal() 1015 { 1016 return [ 1017 [-4], 1018 ['Céphalopodes'], 1019 ]; 1020 } 1021 1022 public function testDisableOutputDisablesTheOutput() 1023 { 1024 $p = $this->getProcess('foo'); 1025 $this->assertFalse($p->isOutputDisabled()); 1026 $p->disableOutput(); 1027 $this->assertTrue($p->isOutputDisabled()); 1028 $p->enableOutput(); 1029 $this->assertFalse($p->isOutputDisabled()); 1030 } 1031 1032 public function testDisableOutputWhileRunningThrowsException() 1033 { 1034 $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); 1035 $this->expectExceptionMessage('Disabling output while the process is running is not possible.'); 1036 $p = $this->getProcessForCode('sleep(39);'); 1037 $p->start(); 1038 $p->disableOutput(); 1039 } 1040 1041 public function testEnableOutputWhileRunningThrowsException() 1042 { 1043 $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); 1044 $this->expectExceptionMessage('Enabling output while the process is running is not possible.'); 1045 $p = $this->getProcessForCode('sleep(40);'); 1046 $p->disableOutput(); 1047 $p->start(); 1048 $p->enableOutput(); 1049 } 1050 1051 public function testEnableOrDisableOutputAfterRunDoesNotThrowException() 1052 { 1053 $p = $this->getProcess('echo foo'); 1054 $p->disableOutput(); 1055 $p->run(); 1056 $p->enableOutput(); 1057 $p->disableOutput(); 1058 $this->assertTrue($p->isOutputDisabled()); 1059 } 1060 1061 public function testDisableOutputWhileIdleTimeoutIsSet() 1062 { 1063 $this->expectException('Symfony\Component\Process\Exception\LogicException'); 1064 $this->expectExceptionMessage('Output can not be disabled while an idle timeout is set.'); 1065 $process = $this->getProcess('foo'); 1066 $process->setIdleTimeout(1); 1067 $process->disableOutput(); 1068 } 1069 1070 public function testSetIdleTimeoutWhileOutputIsDisabled() 1071 { 1072 $this->expectException('Symfony\Component\Process\Exception\LogicException'); 1073 $this->expectExceptionMessage('timeout can not be set while the output is disabled.'); 1074 $process = $this->getProcess('foo'); 1075 $process->disableOutput(); 1076 $process->setIdleTimeout(1); 1077 } 1078 1079 public function testSetNullIdleTimeoutWhileOutputIsDisabled() 1080 { 1081 $process = $this->getProcess('foo'); 1082 $process->disableOutput(); 1083 $this->assertSame($process, $process->setIdleTimeout(null)); 1084 } 1085 1086 /** 1087 * @dataProvider provideOutputFetchingMethods 1088 */ 1089 public function testGetOutputWhileDisabled($fetchMethod) 1090 { 1091 $this->expectException('Symfony\Component\Process\Exception\LogicException'); 1092 $this->expectExceptionMessage('Output has been disabled.'); 1093 $p = $this->getProcessForCode('sleep(41);'); 1094 $p->disableOutput(); 1095 $p->start(); 1096 $p->{$fetchMethod}(); 1097 } 1098 1099 public function provideOutputFetchingMethods() 1100 { 1101 return [ 1102 ['getOutput'], 1103 ['getIncrementalOutput'], 1104 ['getErrorOutput'], 1105 ['getIncrementalErrorOutput'], 1106 ]; 1107 } 1108 1109 public function testStopTerminatesProcessCleanly() 1110 { 1111 $process = $this->getProcessForCode('echo 123; sleep(42);'); 1112 $process->run(function () use ($process) { 1113 $process->stop(); 1114 }); 1115 $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException'); 1116 } 1117 1118 public function testKillSignalTerminatesProcessCleanly() 1119 { 1120 $process = $this->getProcessForCode('echo 123; sleep(43);'); 1121 $process->run(function () use ($process) { 1122 $process->signal(9); // SIGKILL 1123 }); 1124 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); 1125 } 1126 1127 public function testTermSignalTerminatesProcessCleanly() 1128 { 1129 $process = $this->getProcessForCode('echo 123; sleep(44);'); 1130 $process->run(function () use ($process) { 1131 $process->signal(15); // SIGTERM 1132 }); 1133 $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); 1134 } 1135 1136 public function responsesCodeProvider() 1137 { 1138 return [ 1139 //expected output / getter / code to execute 1140 //[1,'getExitCode','exit(1);'], 1141 //[true,'isSuccessful','exit();'], 1142 ['output', 'getOutput', 'echo \'output\';'], 1143 ]; 1144 } 1145 1146 public function pipesCodeProvider() 1147 { 1148 $variations = [ 1149 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);', 1150 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';', 1151 ]; 1152 1153 if ('\\' === \DIRECTORY_SEPARATOR) { 1154 // Avoid XL buffers on Windows because of https://bugs.php.net/65650 1155 $sizes = [1, 2, 4, 8]; 1156 } else { 1157 $sizes = [1, 16, 64, 1024, 4096]; 1158 } 1159 1160 $codes = []; 1161 foreach ($sizes as $size) { 1162 foreach ($variations as $code) { 1163 $codes[] = [$code, $size]; 1164 } 1165 } 1166 1167 return $codes; 1168 } 1169 1170 /** 1171 * @dataProvider provideVariousIncrementals 1172 */ 1173 public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method) 1174 { 1175 $process = $this->getProcessForCode('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }', null, null, null, null); 1176 $process->start(); 1177 $result = ''; 1178 $limit = microtime(true) + 3; 1179 $expected = '012'; 1180 1181 while ($result !== $expected && microtime(true) < $limit) { 1182 $result .= $process->$method(); 1183 } 1184 1185 $this->assertSame($expected, $result); 1186 $process->stop(); 1187 } 1188 1189 public function provideVariousIncrementals() 1190 { 1191 return [ 1192 ['php://stdout', 'getIncrementalOutput'], 1193 ['php://stderr', 'getIncrementalErrorOutput'], 1194 ]; 1195 } 1196 1197 public function testIteratorInput() 1198 { 1199 $input = function () { 1200 yield 'ping'; 1201 yield 'pong'; 1202 }; 1203 1204 $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);', null, null, $input()); 1205 $process->run(); 1206 $this->assertSame('pingpong', $process->getOutput()); 1207 } 1208 1209 public function testSimpleInputStream() 1210 { 1211 $input = new InputStream(); 1212 1213 $process = $this->getProcessForCode('echo \'ping\'; echo fread(STDIN, 4); echo fread(STDIN, 4);'); 1214 $process->setInput($input); 1215 1216 $process->start(function ($type, $data) use ($input) { 1217 if ('ping' === $data) { 1218 $input->write('pang'); 1219 } elseif (!$input->isClosed()) { 1220 $input->write('pong'); 1221 $input->close(); 1222 } 1223 }); 1224 1225 $process->wait(); 1226 $this->assertSame('pingpangpong', $process->getOutput()); 1227 } 1228 1229 public function testInputStreamWithCallable() 1230 { 1231 $i = 0; 1232 $stream = fopen('php://memory', 'w+'); 1233 $stream = function () use ($stream, &$i) { 1234 if ($i < 3) { 1235 rewind($stream); 1236 fwrite($stream, ++$i); 1237 rewind($stream); 1238 1239 return $stream; 1240 } 1241 1242 return null; 1243 }; 1244 1245 $input = new InputStream(); 1246 $input->onEmpty($stream); 1247 $input->write($stream()); 1248 1249 $process = $this->getProcessForCode('echo fread(STDIN, 3);'); 1250 $process->setInput($input); 1251 $process->start(function ($type, $data) use ($input) { 1252 $input->close(); 1253 }); 1254 1255 $process->wait(); 1256 $this->assertSame('123', $process->getOutput()); 1257 } 1258 1259 public function testInputStreamWithGenerator() 1260 { 1261 $input = new InputStream(); 1262 $input->onEmpty(function ($input) { 1263 yield 'pong'; 1264 $input->close(); 1265 }); 1266 1267 $process = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); 1268 $process->setInput($input); 1269 $process->start(); 1270 $input->write('ping'); 1271 $process->wait(); 1272 $this->assertSame('pingpong', $process->getOutput()); 1273 } 1274 1275 public function testInputStreamOnEmpty() 1276 { 1277 $i = 0; 1278 $input = new InputStream(); 1279 $input->onEmpty(function () use (&$i) { ++$i; }); 1280 1281 $process = $this->getProcessForCode('echo 123; echo fread(STDIN, 1); echo 456;'); 1282 $process->setInput($input); 1283 $process->start(function ($type, $data) use ($input) { 1284 if ('123' === $data) { 1285 $input->close(); 1286 } 1287 }); 1288 $process->wait(); 1289 1290 $this->assertSame(0, $i, 'InputStream->onEmpty callback should be called only when the input *becomes* empty'); 1291 $this->assertSame('123456', $process->getOutput()); 1292 } 1293 1294 public function testIteratorOutput() 1295 { 1296 $input = new InputStream(); 1297 1298 $process = $this->getProcessForCode('fwrite(STDOUT, 123); fwrite(STDERR, 234); flush(); usleep(10000); fwrite(STDOUT, fread(STDIN, 3)); fwrite(STDERR, 456);'); 1299 $process->setInput($input); 1300 $process->start(); 1301 $output = []; 1302 1303 foreach ($process as $type => $data) { 1304 $output[] = [$type, $data]; 1305 break; 1306 } 1307 $expectedOutput = [ 1308 [$process::OUT, '123'], 1309 ]; 1310 $this->assertSame($expectedOutput, $output); 1311 1312 $input->write(345); 1313 1314 foreach ($process as $type => $data) { 1315 $output[] = [$type, $data]; 1316 } 1317 1318 $this->assertSame('', $process->getOutput()); 1319 $this->assertFalse($process->isRunning()); 1320 1321 $expectedOutput = [ 1322 [$process::OUT, '123'], 1323 [$process::ERR, '234'], 1324 [$process::OUT, '345'], 1325 [$process::ERR, '456'], 1326 ]; 1327 $this->assertSame($expectedOutput, $output); 1328 } 1329 1330 public function testNonBlockingNorClearingIteratorOutput() 1331 { 1332 $input = new InputStream(); 1333 1334 $process = $this->getProcessForCode('fwrite(STDOUT, fread(STDIN, 3));'); 1335 $process->setInput($input); 1336 $process->start(); 1337 $output = []; 1338 1339 foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) { 1340 $output[] = [$type, $data]; 1341 break; 1342 } 1343 $expectedOutput = [ 1344 [$process::OUT, ''], 1345 ]; 1346 $this->assertSame($expectedOutput, $output); 1347 1348 $input->write(123); 1349 1350 foreach ($process->getIterator($process::ITER_NON_BLOCKING | $process::ITER_KEEP_OUTPUT) as $type => $data) { 1351 if ('' !== $data) { 1352 $output[] = [$type, $data]; 1353 } 1354 } 1355 1356 $this->assertSame('123', $process->getOutput()); 1357 $this->assertFalse($process->isRunning()); 1358 1359 $expectedOutput = [ 1360 [$process::OUT, ''], 1361 [$process::OUT, '123'], 1362 ]; 1363 $this->assertSame($expectedOutput, $output); 1364 } 1365 1366 public function testChainedProcesses() 1367 { 1368 $p1 = $this->getProcessForCode('fwrite(STDERR, 123); fwrite(STDOUT, 456);'); 1369 $p2 = $this->getProcessForCode('stream_copy_to_stream(STDIN, STDOUT);'); 1370 $p2->setInput($p1); 1371 1372 $p1->start(); 1373 $p2->run(); 1374 1375 $this->assertSame('123', $p1->getErrorOutput()); 1376 $this->assertSame('', $p1->getOutput()); 1377 $this->assertSame('', $p2->getErrorOutput()); 1378 $this->assertSame('456', $p2->getOutput()); 1379 } 1380 1381 public function testSetBadEnv() 1382 { 1383 $process = $this->getProcess('echo hello'); 1384 $process->setEnv(['bad%%' => '123']); 1385 $process->inheritEnvironmentVariables(true); 1386 1387 $process->run(); 1388 1389 $this->assertSame('hello'.\PHP_EOL, $process->getOutput()); 1390 $this->assertSame('', $process->getErrorOutput()); 1391 } 1392 1393 public function testEnvBackupDoesNotDeleteExistingVars() 1394 { 1395 putenv('existing_var=foo'); 1396 $_ENV['existing_var'] = 'foo'; 1397 $process = $this->getProcess('php -r "echo getenv(\'new_test_var\');"'); 1398 $process->setEnv(['existing_var' => 'bar', 'new_test_var' => 'foo']); 1399 $process->inheritEnvironmentVariables(); 1400 1401 $process->run(); 1402 1403 $this->assertSame('foo', $process->getOutput()); 1404 $this->assertSame('foo', getenv('existing_var')); 1405 $this->assertFalse(getenv('new_test_var')); 1406 1407 putenv('existing_var'); 1408 unset($_ENV['existing_var']); 1409 } 1410 1411 public function testEnvIsInherited() 1412 { 1413 $process = $this->getProcessForCode('echo serialize($_SERVER);', null, ['BAR' => 'BAZ', 'EMPTY' => '']); 1414 1415 putenv('FOO=BAR'); 1416 $_ENV['FOO'] = 'BAR'; 1417 1418 $process->run(); 1419 1420 $expected = ['BAR' => 'BAZ', 'EMPTY' => '', 'FOO' => 'BAR']; 1421 $env = array_intersect_key(unserialize($process->getOutput()), $expected); 1422 1423 $this->assertEquals($expected, $env); 1424 1425 putenv('FOO'); 1426 unset($_ENV['FOO']); 1427 } 1428 1429 /** 1430 * @group legacy 1431 */ 1432 public function testInheritEnvDisabled() 1433 { 1434 $process = $this->getProcessForCode('echo serialize($_SERVER);', null, ['BAR' => 'BAZ']); 1435 1436 putenv('FOO=BAR'); 1437 $_ENV['FOO'] = 'BAR'; 1438 1439 $this->assertSame($process, $process->inheritEnvironmentVariables(false)); 1440 $this->assertFalse($process->areEnvironmentVariablesInherited()); 1441 1442 $process->run(); 1443 1444 $expected = ['BAR' => 'BAZ', 'FOO' => 'BAR']; 1445 $env = array_intersect_key(unserialize($process->getOutput()), $expected); 1446 unset($expected['FOO']); 1447 1448 $this->assertSame($expected, $env); 1449 1450 putenv('FOO'); 1451 unset($_ENV['FOO']); 1452 } 1453 1454 public function testGetCommandLine() 1455 { 1456 $p = new Process(['/usr/bin/php']); 1457 1458 $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'"; 1459 $this->assertSame($expected, $p->getCommandLine()); 1460 } 1461 1462 /** 1463 * @dataProvider provideEscapeArgument 1464 */ 1465 public function testEscapeArgument($arg) 1466 { 1467 $p = new Process([self::$phpBin, '-r', 'echo $argv[1];', $arg]); 1468 $p->run(); 1469 1470 $this->assertSame((string) $arg, $p->getOutput()); 1471 } 1472 1473 /** 1474 * @dataProvider provideEscapeArgument 1475 * @group legacy 1476 */ 1477 public function testEscapeArgumentWhenInheritEnvDisabled($arg) 1478 { 1479 $p = new Process([self::$phpBin, '-r', 'echo $argv[1];', $arg], null, ['BAR' => 'BAZ']); 1480 $p->inheritEnvironmentVariables(false); 1481 $p->run(); 1482 1483 $this->assertSame((string) $arg, $p->getOutput()); 1484 } 1485 1486 public function testRawCommandLine() 1487 { 1488 $p = new Process(sprintf('"%s" -r %s "a" "" "b"', self::$phpBin, escapeshellarg('print_r($argv);'))); 1489 $p->run(); 1490 1491 $expected = <<<EOTXT 1492Array 1493( 1494 [0] => - 1495 [1] => a 1496 [2] => 1497 [3] => b 1498) 1499 1500EOTXT; 1501 $this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput())); 1502 } 1503 1504 public function provideEscapeArgument() 1505 { 1506 yield ['a"b%c%']; 1507 yield ['a"b^c^']; 1508 yield ["a\nb'c"]; 1509 yield ['a^b c!']; 1510 yield ["a!b\tc"]; 1511 yield ['a\\\\"\\"']; 1512 yield ['éÉèÈàÀöä']; 1513 yield [null]; 1514 yield [1]; 1515 yield [1.1]; 1516 } 1517 1518 public function testEnvArgument() 1519 { 1520 $env = ['FOO' => 'Foo', 'BAR' => 'Bar']; 1521 $cmd = '\\' === \DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ'; 1522 $p = new Process($cmd, null, $env); 1523 $p->run(null, ['BAR' => 'baR', 'BAZ' => 'baZ']); 1524 1525 $this->assertSame('Foo baR baZ', rtrim($p->getOutput())); 1526 $this->assertSame($env, $p->getEnv()); 1527 } 1528 1529 public function testWaitStoppedDeadProcess() 1530 { 1531 $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/ErrorProcessInitiator.php -e '.self::$phpBin); 1532 $process->start(); 1533 $process->setTimeout(2); 1534 $process->wait(); 1535 $this->assertFalse($process->isRunning()); 1536 } 1537 1538 /** 1539 * @param string $commandline 1540 * @param string|null $cwd 1541 * @param string|null $input 1542 * @param int $timeout 1543 * 1544 * @return Process 1545 */ 1546 private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60) 1547 { 1548 $process = new Process($commandline, $cwd, $env, $input, $timeout); 1549 $process->inheritEnvironmentVariables(); 1550 1551 if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) { 1552 try { 1553 $process->setEnhanceSigchildCompatibility(false); 1554 $process->getExitCode(); 1555 $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.'); 1556 } catch (RuntimeException $e) { 1557 $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage()); 1558 if ($enhance) { 1559 $process->setEnhanceSigchildCompatibility(true); 1560 } else { 1561 self::$notEnhancedSigchild = true; 1562 } 1563 } 1564 } 1565 1566 if (self::$process) { 1567 self::$process->stop(0); 1568 } 1569 1570 return self::$process = $process; 1571 } 1572 1573 /** 1574 * @return Process 1575 */ 1576 private function getProcessForCode($code, $cwd = null, array $env = null, $input = null, $timeout = 60) 1577 { 1578 return $this->getProcess([self::$phpBin, '-r', $code], $cwd, $env, $input, $timeout); 1579 } 1580 1581 private function skipIfNotEnhancedSigchild($expectException = true) 1582 { 1583 if (self::$sigchild) { 1584 if (!$expectException) { 1585 $this->markTestSkipped('PHP is compiled with --enable-sigchild.'); 1586 } elseif (self::$notEnhancedSigchild) { 1587 $this->expectException('Symfony\Component\Process\Exception\RuntimeException'); 1588 $this->expectExceptionMessage('This PHP has been compiled with --enable-sigchild.'); 1589 } 1590 } 1591 } 1592} 1593 1594class NonStringifiable 1595{ 1596} 1597