ProcessTest.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Process\Tests;
  11. use Symfony\Component\Process\Exception\LogicException;
  12. use Symfony\Component\Process\Exception\ProcessTimedOutException;
  13. use Symfony\Component\Process\Exception\RuntimeException;
  14. use Symfony\Component\Process\PhpExecutableFinder;
  15. use Symfony\Component\Process\Pipes\PipesInterface;
  16. use Symfony\Component\Process\Process;
  17. /**
  18. * @author Robert Schönthal <seroscho@googlemail.com>
  19. */
  20. class ProcessTest extends \PHPUnit_Framework_TestCase
  21. {
  22. private static $phpBin;
  23. private static $process;
  24. private static $sigchild;
  25. private static $notEnhancedSigchild = false;
  26. public static function setUpBeforeClass()
  27. {
  28. $phpBin = new PhpExecutableFinder();
  29. self::$phpBin = getenv('SYMFONY_PROCESS_PHP_TEST_BINARY') ?: ('phpdbg' === PHP_SAPI ? 'php' : $phpBin->find());
  30. if ('\\' !== DIRECTORY_SEPARATOR) {
  31. // exec is mandatory to deal with sending a signal to the process
  32. // see https://github.com/symfony/symfony/issues/5030 about prepending
  33. // command with exec
  34. self::$phpBin = 'exec '.self::$phpBin;
  35. }
  36. ob_start();
  37. phpinfo(INFO_GENERAL);
  38. self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
  39. }
  40. protected function tearDown()
  41. {
  42. if (self::$process) {
  43. self::$process->stop(0);
  44. self::$process = null;
  45. }
  46. }
  47. public function testThatProcessDoesNotThrowWarningDuringRun()
  48. {
  49. if ('\\' === DIRECTORY_SEPARATOR) {
  50. $this->markTestSkipped('This test is transient on Windows');
  51. }
  52. @trigger_error('Test Error', E_USER_NOTICE);
  53. $process = $this->getProcess(self::$phpBin." -r 'sleep(3)'");
  54. $process->run();
  55. $actualError = error_get_last();
  56. $this->assertEquals('Test Error', $actualError['message']);
  57. $this->assertEquals(E_USER_NOTICE, $actualError['type']);
  58. }
  59. /**
  60. * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
  61. */
  62. public function testNegativeTimeoutFromConstructor()
  63. {
  64. $this->getProcess('', null, null, null, -1);
  65. }
  66. /**
  67. * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
  68. */
  69. public function testNegativeTimeoutFromSetter()
  70. {
  71. $p = $this->getProcess('');
  72. $p->setTimeout(-1);
  73. }
  74. public function testFloatAndNullTimeout()
  75. {
  76. $p = $this->getProcess('');
  77. $p->setTimeout(10);
  78. $this->assertSame(10.0, $p->getTimeout());
  79. $p->setTimeout(null);
  80. $this->assertNull($p->getTimeout());
  81. $p->setTimeout(0.0);
  82. $this->assertNull($p->getTimeout());
  83. }
  84. /**
  85. * @requires extension pcntl
  86. */
  87. public function testStopWithTimeoutIsActuallyWorking()
  88. {
  89. $p = $this->getProcess(self::$phpBin.' '.__DIR__.'/NonStopableProcess.php 30');
  90. $p->start();
  91. while (false === strpos($p->getOutput(), 'received')) {
  92. usleep(1000);
  93. }
  94. $start = microtime(true);
  95. $p->stop(0.1);
  96. $p->wait();
  97. $this->assertLessThan(15, microtime(true) - $start);
  98. }
  99. public function testAllOutputIsActuallyReadOnTermination()
  100. {
  101. // this code will result in a maximum of 2 reads of 8192 bytes by calling
  102. // start() and isRunning(). by the time getOutput() is called the process
  103. // has terminated so the internal pipes array is already empty. normally
  104. // the call to start() will not read any data as the process will not have
  105. // generated output, but this is non-deterministic so we must count it as
  106. // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus
  107. // another byte which will never be read.
  108. $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2;
  109. $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize);
  110. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
  111. $p->start();
  112. // Don't call Process::run nor Process::wait to avoid any read of pipes
  113. $h = new \ReflectionProperty($p, 'process');
  114. $h->setAccessible(true);
  115. $h = $h->getValue($p);
  116. $s = @proc_get_status($h);
  117. while (!empty($s['running'])) {
  118. usleep(1000);
  119. $s = proc_get_status($h);
  120. }
  121. $o = $p->getOutput();
  122. $this->assertEquals($expectedOutputSize, strlen($o));
  123. }
  124. public function testCallbacksAreExecutedWithStart()
  125. {
  126. $process = $this->getProcess('echo foo');
  127. $process->start(function ($type, $buffer) use (&$data) {
  128. $data .= $buffer;
  129. });
  130. $process->wait();
  131. $this->assertSame('foo'.PHP_EOL, $data);
  132. }
  133. /**
  134. * tests results from sub processes.
  135. *
  136. * @dataProvider responsesCodeProvider
  137. */
  138. public function testProcessResponses($expected, $getter, $code)
  139. {
  140. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
  141. $p->run();
  142. $this->assertSame($expected, $p->$getter());
  143. }
  144. /**
  145. * tests results from sub processes.
  146. *
  147. * @dataProvider pipesCodeProvider
  148. */
  149. public function testProcessPipes($code, $size)
  150. {
  151. $expected = str_repeat(str_repeat('*', 1024), $size).'!';
  152. $expectedLength = (1024 * $size) + 1;
  153. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
  154. $p->setInput($expected);
  155. $p->run();
  156. $this->assertEquals($expectedLength, strlen($p->getOutput()));
  157. $this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
  158. }
  159. /**
  160. * @dataProvider pipesCodeProvider
  161. */
  162. public function testSetStreamAsInput($code, $size)
  163. {
  164. $expected = str_repeat(str_repeat('*', 1024), $size).'!';
  165. $expectedLength = (1024 * $size) + 1;
  166. $stream = fopen('php://temporary', 'w+');
  167. fwrite($stream, $expected);
  168. rewind($stream);
  169. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)));
  170. $p->setInput($stream);
  171. $p->run();
  172. fclose($stream);
  173. $this->assertEquals($expectedLength, strlen($p->getOutput()));
  174. $this->assertEquals($expectedLength, strlen($p->getErrorOutput()));
  175. }
  176. public function testLiveStreamAsInput()
  177. {
  178. $stream = fopen('php://memory', 'r+');
  179. fwrite($stream, 'hello');
  180. rewind($stream);
  181. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('stream_copy_to_stream(STDIN, STDOUT);')));
  182. $p->setInput($stream);
  183. $p->start(function ($type, $data) use ($stream) {
  184. if ('hello' === $data) {
  185. fclose($stream);
  186. }
  187. });
  188. $p->wait();
  189. $this->assertSame('hello', $p->getOutput());
  190. }
  191. /**
  192. * @expectedException \Symfony\Component\Process\Exception\LogicException
  193. * @expectedExceptionMessage Input can not be set while the process is running.
  194. */
  195. public function testSetInputWhileRunningThrowsAnException()
  196. {
  197. $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
  198. $process->start();
  199. try {
  200. $process->setInput('foobar');
  201. $process->stop();
  202. $this->fail('A LogicException should have been raised.');
  203. } catch (LogicException $e) {
  204. }
  205. $process->stop();
  206. throw $e;
  207. }
  208. /**
  209. * @dataProvider provideInvalidInputValues
  210. * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException
  211. * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings or stream resources.
  212. */
  213. public function testInvalidInput($value)
  214. {
  215. $process = $this->getProcess('foo');
  216. $process->setInput($value);
  217. }
  218. public function provideInvalidInputValues()
  219. {
  220. return array(
  221. array(array()),
  222. array(new NonStringifiable()),
  223. );
  224. }
  225. /**
  226. * @dataProvider provideInputValues
  227. */
  228. public function testValidInput($expected, $value)
  229. {
  230. $process = $this->getProcess('foo');
  231. $process->setInput($value);
  232. $this->assertSame($expected, $process->getInput());
  233. }
  234. public function provideInputValues()
  235. {
  236. return array(
  237. array(null, null),
  238. array('24.5', 24.5),
  239. array('input data', 'input data'),
  240. );
  241. }
  242. public function chainedCommandsOutputProvider()
  243. {
  244. if ('\\' === DIRECTORY_SEPARATOR) {
  245. return array(
  246. array("2 \r\n2\r\n", '&&', '2'),
  247. );
  248. }
  249. return array(
  250. array("1\n1\n", ';', '1'),
  251. array("2\n2\n", '&&', '2'),
  252. );
  253. }
  254. /**
  255. * @dataProvider chainedCommandsOutputProvider
  256. */
  257. public function testChainedCommandsOutput($expected, $operator, $input)
  258. {
  259. $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input));
  260. $process->run();
  261. $this->assertEquals($expected, $process->getOutput());
  262. }
  263. public function testCallbackIsExecutedForOutput()
  264. {
  265. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';')));
  266. $called = false;
  267. $p->run(function ($type, $buffer) use (&$called) {
  268. $called = $buffer === 'foo';
  269. });
  270. $this->assertTrue($called, 'The callback should be executed with the output');
  271. }
  272. public function testGetErrorOutput()
  273. {
  274. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }')));
  275. $p->run();
  276. $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches));
  277. }
  278. public function testFlushErrorOutput()
  279. {
  280. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }')));
  281. $p->run();
  282. $p->clearErrorOutput();
  283. $this->assertEmpty($p->getErrorOutput());
  284. }
  285. /**
  286. * @dataProvider provideIncrementalOutput
  287. */
  288. public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri)
  289. {
  290. $lock = tempnam(sys_get_temp_dir(), __FUNCTION__);
  291. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('file_put_contents($s = \''.$uri.'\', \'foo\'); flock(fopen('.var_export($lock, true).', \'r\'), LOCK_EX); file_put_contents($s, \'bar\');')));
  292. $h = fopen($lock, 'w');
  293. flock($h, LOCK_EX);
  294. $p->start();
  295. foreach (array('foo', 'bar') as $s) {
  296. while (false === strpos($p->$getOutput(), $s)) {
  297. usleep(1000);
  298. }
  299. $this->assertSame($s, $p->$getIncrementalOutput());
  300. $this->assertSame('', $p->$getIncrementalOutput());
  301. flock($h, LOCK_UN);
  302. }
  303. fclose($h);
  304. }
  305. public function provideIncrementalOutput()
  306. {
  307. return array(
  308. array('getOutput', 'getIncrementalOutput', 'php://stdout'),
  309. array('getErrorOutput', 'getIncrementalErrorOutput', 'php://stderr'),
  310. );
  311. }
  312. public function testGetOutput()
  313. {
  314. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }')));
  315. $p->run();
  316. $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches));
  317. }
  318. public function testFlushOutput()
  319. {
  320. $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}')));
  321. $p->run();
  322. $p->clearOutput();
  323. $this->assertEmpty($p->getOutput());
  324. }
  325. public function testZeroAsOutput()
  326. {
  327. if ('\\' === DIRECTORY_SEPARATOR) {
  328. // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line
  329. $p = $this->getProcess('echo | set /p dummyName=0');
  330. } else {
  331. $p = $this->getProcess('printf 0');
  332. }
  333. $p->run();
  334. $this->assertSame('0', $p->getOutput());
  335. }
  336. public function testExitCodeCommandFailed()
  337. {
  338. if ('\\' === DIRECTORY_SEPARATOR) {
  339. $this->markTestSkipped('Windows does not support POSIX exit code');
  340. }
  341. $this->skipIfNotEnhancedSigchild();
  342. // such command run in bash return an exitcode 127
  343. $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis');
  344. $process->run();
  345. $this->assertGreaterThan(0, $process->getExitCode());
  346. }
  347. public function testTTYCommand()
  348. {
  349. if ('\\' === DIRECTORY_SEPARATOR) {
  350. $this->markTestSkipped('Windows does not have /dev/tty support');
  351. }
  352. $process = $this->getProcess('echo "foo" >> /dev/null && '.self::$phpBin.' -r "usleep(100000);"');
  353. $process->setTty(true);
  354. $process->start();
  355. $this->assertTrue($process->isRunning());
  356. $process->wait();
  357. $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
  358. }
  359. public function testTTYCommandExitCode()
  360. {
  361. if ('\\' === DIRECTORY_SEPARATOR) {
  362. $this->markTestSkipped('Windows does have /dev/tty support');
  363. }
  364. $this->skipIfNotEnhancedSigchild();
  365. $process = $this->getProcess('echo "foo" >> /dev/null');
  366. $process->setTty(true);
  367. $process->run();
  368. $this->assertTrue($process->isSuccessful());
  369. }
  370. /**
  371. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  372. * @expectedExceptionMessage TTY mode is not supported on Windows platform.
  373. */
  374. public function testTTYInWindowsEnvironment()
  375. {
  376. if ('\\' !== DIRECTORY_SEPARATOR) {
  377. $this->markTestSkipped('This test is for Windows platform only');
  378. }
  379. $process = $this->getProcess('echo "foo" >> /dev/null');
  380. $process->setTty(false);
  381. $process->setTty(true);
  382. }
  383. public function testExitCodeTextIsNullWhenExitCodeIsNull()
  384. {
  385. $this->skipIfNotEnhancedSigchild();
  386. $process = $this->getProcess('');
  387. $this->assertNull($process->getExitCodeText());
  388. }
  389. public function testPTYCommand()
  390. {
  391. if (!Process::isPtySupported()) {
  392. $this->markTestSkipped('PTY is not supported on this operating system.');
  393. }
  394. $process = $this->getProcess('echo "foo"');
  395. $process->setPty(true);
  396. $process->run();
  397. $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
  398. $this->assertEquals("foo\r\n", $process->getOutput());
  399. }
  400. public function testMustRun()
  401. {
  402. $this->skipIfNotEnhancedSigchild();
  403. $process = $this->getProcess('echo foo');
  404. $this->assertSame($process, $process->mustRun());
  405. $this->assertEquals('foo'.PHP_EOL, $process->getOutput());
  406. }
  407. public function testSuccessfulMustRunHasCorrectExitCode()
  408. {
  409. $this->skipIfNotEnhancedSigchild();
  410. $process = $this->getProcess('echo foo')->mustRun();
  411. $this->assertEquals(0, $process->getExitCode());
  412. }
  413. /**
  414. * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException
  415. */
  416. public function testMustRunThrowsException()
  417. {
  418. $this->skipIfNotEnhancedSigchild();
  419. $process = $this->getProcess('exit 1');
  420. $process->mustRun();
  421. }
  422. public function testExitCodeText()
  423. {
  424. $this->skipIfNotEnhancedSigchild();
  425. $process = $this->getProcess('');
  426. $r = new \ReflectionObject($process);
  427. $p = $r->getProperty('exitcode');
  428. $p->setAccessible(true);
  429. $p->setValue($process, 2);
  430. $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText());
  431. }
  432. public function testStartIsNonBlocking()
  433. {
  434. $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"');
  435. $start = microtime(true);
  436. $process->start();
  437. $end = microtime(true);
  438. $this->assertLessThan(0.4, $end - $start);
  439. $process->stop();
  440. }
  441. public function testUpdateStatus()
  442. {
  443. $process = $this->getProcess('echo foo');
  444. $process->run();
  445. $this->assertTrue(strlen($process->getOutput()) > 0);
  446. }
  447. public function testGetExitCodeIsNullOnStart()
  448. {
  449. $this->skipIfNotEnhancedSigchild();
  450. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  451. $this->assertNull($process->getExitCode());
  452. $process->start();
  453. $this->assertNull($process->getExitCode());
  454. $process->wait();
  455. $this->assertEquals(0, $process->getExitCode());
  456. }
  457. public function testGetExitCodeIsNullOnWhenStartingAgain()
  458. {
  459. $this->skipIfNotEnhancedSigchild();
  460. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  461. $process->run();
  462. $this->assertEquals(0, $process->getExitCode());
  463. $process->start();
  464. $this->assertNull($process->getExitCode());
  465. $process->wait();
  466. $this->assertEquals(0, $process->getExitCode());
  467. }
  468. public function testGetExitCode()
  469. {
  470. $this->skipIfNotEnhancedSigchild();
  471. $process = $this->getProcess('echo foo');
  472. $process->run();
  473. $this->assertSame(0, $process->getExitCode());
  474. }
  475. public function testStatus()
  476. {
  477. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  478. $this->assertFalse($process->isRunning());
  479. $this->assertFalse($process->isStarted());
  480. $this->assertFalse($process->isTerminated());
  481. $this->assertSame(Process::STATUS_READY, $process->getStatus());
  482. $process->start();
  483. $this->assertTrue($process->isRunning());
  484. $this->assertTrue($process->isStarted());
  485. $this->assertFalse($process->isTerminated());
  486. $this->assertSame(Process::STATUS_STARTED, $process->getStatus());
  487. $process->wait();
  488. $this->assertFalse($process->isRunning());
  489. $this->assertTrue($process->isStarted());
  490. $this->assertTrue($process->isTerminated());
  491. $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus());
  492. }
  493. public function testStop()
  494. {
  495. $process = $this->getProcess(self::$phpBin.' -r "sleep(31);"');
  496. $process->start();
  497. $this->assertTrue($process->isRunning());
  498. $process->stop();
  499. $this->assertFalse($process->isRunning());
  500. }
  501. public function testIsSuccessful()
  502. {
  503. $this->skipIfNotEnhancedSigchild();
  504. $process = $this->getProcess('echo foo');
  505. $process->run();
  506. $this->assertTrue($process->isSuccessful());
  507. }
  508. public function testIsSuccessfulOnlyAfterTerminated()
  509. {
  510. $this->skipIfNotEnhancedSigchild();
  511. $process = $this->getProcess(self::$phpBin.' -r "usleep(100000);"');
  512. $process->start();
  513. $this->assertFalse($process->isSuccessful());
  514. $process->wait();
  515. $this->assertTrue($process->isSuccessful());
  516. }
  517. public function testIsNotSuccessful()
  518. {
  519. $this->skipIfNotEnhancedSigchild();
  520. $process = $this->getProcess(self::$phpBin.' -r "throw new \Exception(\'BOUM\');"');
  521. $process->run();
  522. $this->assertFalse($process->isSuccessful());
  523. }
  524. public function testProcessIsNotSignaled()
  525. {
  526. if ('\\' === DIRECTORY_SEPARATOR) {
  527. $this->markTestSkipped('Windows does not support POSIX signals');
  528. }
  529. $this->skipIfNotEnhancedSigchild();
  530. $process = $this->getProcess('echo foo');
  531. $process->run();
  532. $this->assertFalse($process->hasBeenSignaled());
  533. }
  534. public function testProcessWithoutTermSignal()
  535. {
  536. if ('\\' === DIRECTORY_SEPARATOR) {
  537. $this->markTestSkipped('Windows does not support POSIX signals');
  538. }
  539. $this->skipIfNotEnhancedSigchild();
  540. $process = $this->getProcess('echo foo');
  541. $process->run();
  542. $this->assertEquals(0, $process->getTermSignal());
  543. }
  544. public function testProcessIsSignaledIfStopped()
  545. {
  546. if ('\\' === DIRECTORY_SEPARATOR) {
  547. $this->markTestSkipped('Windows does not support POSIX signals');
  548. }
  549. $this->skipIfNotEnhancedSigchild();
  550. $process = $this->getProcess(self::$phpBin.' -r "sleep(32);"');
  551. $process->start();
  552. $process->stop();
  553. $this->assertTrue($process->hasBeenSignaled());
  554. $this->assertEquals(15, $process->getTermSignal()); // SIGTERM
  555. }
  556. /**
  557. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  558. * @expectedExceptionMessage The process has been signaled
  559. */
  560. public function testProcessThrowsExceptionWhenExternallySignaled()
  561. {
  562. if (!function_exists('posix_kill')) {
  563. $this->markTestSkipped('Function posix_kill is required.');
  564. }
  565. $this->skipIfNotEnhancedSigchild(false);
  566. $process = $this->getProcess(self::$phpBin.' -r "sleep(32.1)"');
  567. $process->start();
  568. posix_kill($process->getPid(), 9); // SIGKILL
  569. $process->wait();
  570. }
  571. public function testRestart()
  572. {
  573. $process1 = $this->getProcess(self::$phpBin.' -r "echo getmypid();"');
  574. $process1->run();
  575. $process2 = $process1->restart();
  576. $process2->wait(); // wait for output
  577. // Ensure that both processed finished and the output is numeric
  578. $this->assertFalse($process1->isRunning());
  579. $this->assertFalse($process2->isRunning());
  580. $this->assertTrue(is_numeric($process1->getOutput()));
  581. $this->assertTrue(is_numeric($process2->getOutput()));
  582. // Ensure that restart returned a new process by check that the output is different
  583. $this->assertNotEquals($process1->getOutput(), $process2->getOutput());
  584. }
  585. /**
  586. * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
  587. * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
  588. */
  589. public function testRunProcessWithTimeout()
  590. {
  591. $process = $this->getProcess(self::$phpBin.' -r "sleep(30);"');
  592. $process->setTimeout(0.1);
  593. $start = microtime(true);
  594. try {
  595. $process->run();
  596. $this->fail('A RuntimeException should have been raised');
  597. } catch (RuntimeException $e) {
  598. }
  599. $this->assertLessThan(15, microtime(true) - $start);
  600. throw $e;
  601. }
  602. public function testCheckTimeoutOnNonStartedProcess()
  603. {
  604. $process = $this->getProcess('echo foo');
  605. $this->assertNull($process->checkTimeout());
  606. }
  607. public function testCheckTimeoutOnTerminatedProcess()
  608. {
  609. $process = $this->getProcess('echo foo');
  610. $process->run();
  611. $this->assertNull($process->checkTimeout());
  612. }
  613. /**
  614. * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
  615. * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
  616. */
  617. public function testCheckTimeoutOnStartedProcess()
  618. {
  619. $process = $this->getProcess(self::$phpBin.' -r "sleep(33);"');
  620. $process->setTimeout(0.1);
  621. $process->start();
  622. $start = microtime(true);
  623. try {
  624. while ($process->isRunning()) {
  625. $process->checkTimeout();
  626. usleep(100000);
  627. }
  628. $this->fail('A ProcessTimedOutException should have been raised');
  629. } catch (ProcessTimedOutException $e) {
  630. }
  631. $this->assertLessThan(15, microtime(true) - $start);
  632. throw $e;
  633. }
  634. public function testIdleTimeout()
  635. {
  636. $process = $this->getProcess(self::$phpBin.' -r "sleep(34);"');
  637. $process->setTimeout(60);
  638. $process->setIdleTimeout(0.1);
  639. try {
  640. $process->run();
  641. $this->fail('A timeout exception was expected.');
  642. } catch (ProcessTimedOutException $e) {
  643. $this->assertTrue($e->isIdleTimeout());
  644. $this->assertFalse($e->isGeneralTimeout());
  645. $this->assertEquals(0.1, $e->getExceededTimeout());
  646. }
  647. }
  648. public function testIdleTimeoutNotExceededWhenOutputIsSent()
  649. {
  650. $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('while (true) {echo \'foo \'; usleep(1000);}')));
  651. $process->setTimeout(1);
  652. $process->start();
  653. while (false === strpos($process->getOutput(), 'foo')) {
  654. usleep(1000);
  655. }
  656. $process->setIdleTimeout(0.5);
  657. try {
  658. $process->wait();
  659. $this->fail('A timeout exception was expected.');
  660. } catch (ProcessTimedOutException $e) {
  661. $this->assertTrue($e->isGeneralTimeout(), 'A general timeout is expected.');
  662. $this->assertFalse($e->isIdleTimeout(), 'No idle timeout is expected.');
  663. $this->assertEquals(1, $e->getExceededTimeout());
  664. }
  665. }
  666. /**
  667. * @expectedException \Symfony\Component\Process\Exception\ProcessTimedOutException
  668. * @expectedExceptionMessage exceeded the timeout of 0.1 seconds.
  669. */
  670. public function testStartAfterATimeout()
  671. {
  672. $process = $this->getProcess(self::$phpBin.' -r "sleep(35);"');
  673. $process->setTimeout(0.1);
  674. try {
  675. $process->run();
  676. $this->fail('A ProcessTimedOutException should have been raised.');
  677. } catch (ProcessTimedOutException $e) {
  678. }
  679. $this->assertFalse($process->isRunning());
  680. $process->start();
  681. $this->assertTrue($process->isRunning());
  682. $process->stop(0);
  683. throw $e;
  684. }
  685. public function testGetPid()
  686. {
  687. $process = $this->getProcess(self::$phpBin.' -r "sleep(36);"');
  688. $process->start();
  689. $this->assertGreaterThan(0, $process->getPid());
  690. $process->stop(0);
  691. }
  692. public function testGetPidIsNullBeforeStart()
  693. {
  694. $process = $this->getProcess('foo');
  695. $this->assertNull($process->getPid());
  696. }
  697. public function testGetPidIsNullAfterRun()
  698. {
  699. $process = $this->getProcess('echo foo');
  700. $process->run();
  701. $this->assertNull($process->getPid());
  702. }
  703. /**
  704. * @requires extension pcntl
  705. */
  706. public function testSignal()
  707. {
  708. $process = $this->getProcess(self::$phpBin.' '.__DIR__.'/SignalListener.php');
  709. $process->start();
  710. while (false === strpos($process->getOutput(), 'Caught')) {
  711. usleep(1000);
  712. }
  713. $process->signal(SIGUSR1);
  714. $process->wait();
  715. $this->assertEquals('Caught SIGUSR1', $process->getOutput());
  716. }
  717. /**
  718. * @requires extension pcntl
  719. */
  720. public function testExitCodeIsAvailableAfterSignal()
  721. {
  722. $this->skipIfNotEnhancedSigchild();
  723. $process = $this->getProcess('sleep 4');
  724. $process->start();
  725. $process->signal(SIGKILL);
  726. while ($process->isRunning()) {
  727. usleep(10000);
  728. }
  729. $this->assertFalse($process->isRunning());
  730. $this->assertTrue($process->hasBeenSignaled());
  731. $this->assertFalse($process->isSuccessful());
  732. $this->assertEquals(137, $process->getExitCode());
  733. }
  734. /**
  735. * @expectedException \Symfony\Component\Process\Exception\LogicException
  736. * @expectedExceptionMessage Can not send signal on a non running process.
  737. */
  738. public function testSignalProcessNotRunning()
  739. {
  740. $process = $this->getProcess('foo');
  741. $process->signal(1); // SIGHUP
  742. }
  743. /**
  744. * @dataProvider provideMethodsThatNeedARunningProcess
  745. */
  746. public function testMethodsThatNeedARunningProcess($method)
  747. {
  748. $process = $this->getProcess('foo');
  749. $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method));
  750. $process->{$method}();
  751. }
  752. public function provideMethodsThatNeedARunningProcess()
  753. {
  754. return array(
  755. array('getOutput'),
  756. array('getIncrementalOutput'),
  757. array('getErrorOutput'),
  758. array('getIncrementalErrorOutput'),
  759. array('wait'),
  760. );
  761. }
  762. /**
  763. * @dataProvider provideMethodsThatNeedATerminatedProcess
  764. * @expectedException Symfony\Component\Process\Exception\LogicException
  765. * @expectedExceptionMessage Process must be terminated before calling
  766. */
  767. public function testMethodsThatNeedATerminatedProcess($method)
  768. {
  769. $process = $this->getProcess(self::$phpBin.' -r "sleep(37);"');
  770. $process->start();
  771. try {
  772. $process->{$method}();
  773. $process->stop(0);
  774. $this->fail('A LogicException must have been thrown');
  775. } catch (\Exception $e) {
  776. }
  777. $process->stop(0);
  778. throw $e;
  779. }
  780. public function provideMethodsThatNeedATerminatedProcess()
  781. {
  782. return array(
  783. array('hasBeenSignaled'),
  784. array('getTermSignal'),
  785. array('hasBeenStopped'),
  786. array('getStopSignal'),
  787. );
  788. }
  789. /**
  790. * @dataProvider provideWrongSignal
  791. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  792. */
  793. public function testWrongSignal($signal)
  794. {
  795. if ('\\' === DIRECTORY_SEPARATOR) {
  796. $this->markTestSkipped('POSIX signals do not work on Windows');
  797. }
  798. $process = $this->getProcess(self::$phpBin.' -r "sleep(38);"');
  799. $process->start();
  800. try {
  801. $process->signal($signal);
  802. $this->fail('A RuntimeException must have been thrown');
  803. } catch (RuntimeException $e) {
  804. $process->stop(0);
  805. }
  806. throw $e;
  807. }
  808. public function provideWrongSignal()
  809. {
  810. return array(
  811. array(-4),
  812. array('Céphalopodes'),
  813. );
  814. }
  815. public function testDisableOutputDisablesTheOutput()
  816. {
  817. $p = $this->getProcess('foo');
  818. $this->assertFalse($p->isOutputDisabled());
  819. $p->disableOutput();
  820. $this->assertTrue($p->isOutputDisabled());
  821. $p->enableOutput();
  822. $this->assertFalse($p->isOutputDisabled());
  823. }
  824. /**
  825. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  826. * @expectedExceptionMessage Disabling output while the process is running is not possible.
  827. */
  828. public function testDisableOutputWhileRunningThrowsException()
  829. {
  830. $p = $this->getProcess(self::$phpBin.' -r "sleep(39);"');
  831. $p->start();
  832. $p->disableOutput();
  833. }
  834. /**
  835. * @expectedException \Symfony\Component\Process\Exception\RuntimeException
  836. * @expectedExceptionMessage Enabling output while the process is running is not possible.
  837. */
  838. public function testEnableOutputWhileRunningThrowsException()
  839. {
  840. $p = $this->getProcess(self::$phpBin.' -r "sleep(40);"');
  841. $p->disableOutput();
  842. $p->start();
  843. $p->enableOutput();
  844. }
  845. public function testEnableOrDisableOutputAfterRunDoesNotThrowException()
  846. {
  847. $p = $this->getProcess('echo foo');
  848. $p->disableOutput();
  849. $p->run();
  850. $p->enableOutput();
  851. $p->disableOutput();
  852. $this->assertTrue($p->isOutputDisabled());
  853. }
  854. /**
  855. * @expectedException \Symfony\Component\Process\Exception\LogicException
  856. * @expectedExceptionMessage Output can not be disabled while an idle timeout is set.
  857. */
  858. public function testDisableOutputWhileIdleTimeoutIsSet()
  859. {
  860. $process = $this->getProcess('foo');
  861. $process->setIdleTimeout(1);
  862. $process->disableOutput();
  863. }
  864. /**
  865. * @expectedException \Symfony\Component\Process\Exception\LogicException
  866. * @expectedExceptionMessage timeout can not be set while the output is disabled.
  867. */
  868. public function testSetIdleTimeoutWhileOutputIsDisabled()
  869. {
  870. $process = $this->getProcess('foo');
  871. $process->disableOutput();
  872. $process->setIdleTimeout(1);
  873. }
  874. public function testSetNullIdleTimeoutWhileOutputIsDisabled()
  875. {
  876. $process = $this->getProcess('foo');
  877. $process->disableOutput();
  878. $this->assertSame($process, $process->setIdleTimeout(null));
  879. }
  880. /**
  881. * @dataProvider provideStartMethods
  882. */
  883. public function testStartWithACallbackAndDisabledOutput($startMethod, $exception, $exceptionMessage)
  884. {
  885. $p = $this->getProcess('foo');
  886. $p->disableOutput();
  887. $this->setExpectedException($exception, $exceptionMessage);
  888. if ('mustRun' === $startMethod) {
  889. $this->skipIfNotEnhancedSigchild();
  890. }
  891. $p->{$startMethod}(function () {});
  892. }
  893. public function provideStartMethods()
  894. {
  895. return array(
  896. array('start', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
  897. array('run', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
  898. array('mustRun', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'),
  899. );
  900. }
  901. /**
  902. * @dataProvider provideOutputFetchingMethods
  903. * @expectedException \Symfony\Component\Process\Exception\LogicException
  904. * @expectedExceptionMessage Output has been disabled.
  905. */
  906. public function testGetOutputWhileDisabled($fetchMethod)
  907. {
  908. $p = $this->getProcess(self::$phpBin.' -r "sleep(41);"');
  909. $p->disableOutput();
  910. $p->start();
  911. $p->{$fetchMethod}();
  912. }
  913. public function provideOutputFetchingMethods()
  914. {
  915. return array(
  916. array('getOutput'),
  917. array('getIncrementalOutput'),
  918. array('getErrorOutput'),
  919. array('getIncrementalErrorOutput'),
  920. );
  921. }
  922. public function testStopTerminatesProcessCleanly()
  923. {
  924. $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(42);"');
  925. $process->run(function () use ($process) {
  926. $process->stop();
  927. });
  928. $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException');
  929. }
  930. public function testKillSignalTerminatesProcessCleanly()
  931. {
  932. $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(43);"');
  933. $process->run(function () use ($process) {
  934. $process->signal(9); // SIGKILL
  935. });
  936. $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
  937. }
  938. public function testTermSignalTerminatesProcessCleanly()
  939. {
  940. $process = $this->getProcess(self::$phpBin.' -r "echo 123; sleep(44);"');
  941. $process->run(function () use ($process) {
  942. $process->signal(15); // SIGTERM
  943. });
  944. $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException');
  945. }
  946. public function responsesCodeProvider()
  947. {
  948. return array(
  949. //expected output / getter / code to execute
  950. //array(1,'getExitCode','exit(1);'),
  951. //array(true,'isSuccessful','exit();'),
  952. array('output', 'getOutput', 'echo \'output\';'),
  953. );
  954. }
  955. public function pipesCodeProvider()
  956. {
  957. $variations = array(
  958. 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);',
  959. 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';',
  960. );
  961. if ('\\' === DIRECTORY_SEPARATOR) {
  962. // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650
  963. $sizes = array(1, 2, 4, 8);
  964. } else {
  965. $sizes = array(1, 16, 64, 1024, 4096);
  966. }
  967. $codes = array();
  968. foreach ($sizes as $size) {
  969. foreach ($variations as $code) {
  970. $codes[] = array($code, $size);
  971. }
  972. }
  973. return $codes;
  974. }
  975. /**
  976. * @dataProvider provideVariousIncrementals
  977. */
  978. public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method)
  979. {
  980. $process = $this->getProcess(self::$phpBin.' -r '.escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\''.$stream.'\', $n, 1); $n++; usleep(1000); }'), null, null, null, null);
  981. $process->start();
  982. $result = '';
  983. $limit = microtime(true) + 3;
  984. $expected = '012';
  985. while ($result !== $expected && microtime(true) < $limit) {
  986. $result .= $process->$method();
  987. }
  988. $this->assertSame($expected, $result);
  989. $process->stop();
  990. }
  991. public function provideVariousIncrementals()
  992. {
  993. return array(
  994. array('php://stdout', 'getIncrementalOutput'),
  995. array('php://stderr', 'getIncrementalErrorOutput'),
  996. );
  997. }
  998. /**
  999. * @param string $commandline
  1000. * @param null|string $cwd
  1001. * @param null|array $env
  1002. * @param null|string $input
  1003. * @param int $timeout
  1004. * @param array $options
  1005. *
  1006. * @return Process
  1007. */
  1008. private function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array())
  1009. {
  1010. $process = new Process($commandline, $cwd, $env, $input, $timeout, $options);
  1011. if (false !== $enhance = getenv('ENHANCE_SIGCHLD')) {
  1012. try {
  1013. $process->setEnhanceSigchildCompatibility(false);
  1014. $process->getExitCode();
  1015. $this->fail('ENHANCE_SIGCHLD must be used together with a sigchild-enabled PHP.');
  1016. } catch (RuntimeException $e) {
  1017. $this->assertSame('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.', $e->getMessage());
  1018. if ($enhance) {
  1019. $process->setEnhanceSigchildCompatibility(true);
  1020. } else {
  1021. self::$notEnhancedSigchild = true;
  1022. }
  1023. }
  1024. }
  1025. if (self::$process) {
  1026. self::$process->stop(0);
  1027. }
  1028. return self::$process = $process;
  1029. }
  1030. private function skipIfNotEnhancedSigchild($expectException = true)
  1031. {
  1032. if (self::$sigchild) {
  1033. if (!$expectException) {
  1034. $this->markTestSkipped('PHP is compiled with --enable-sigchild.');
  1035. } elseif (self::$notEnhancedSigchild) {
  1036. $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild.');
  1037. }
  1038. }
  1039. }
  1040. }
  1041. class NonStringifiable
  1042. {
  1043. }