Skip to content

Commit c57fbda

Browse files
committed
bug symfony#10480 [Process] Fixed fatal errors in getOutput and getErrorOutput when process was not started (romainneutron)
This PR was merged into the 2.3 branch. Discussion ---------- [Process] Fixed fatal errors in getOutput and getErrorOutput when process was not started | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | symfony#10022 | License | MIT This PR replaces symfony#9452 and address the latest changes. Side note : I've not updated `getExitCode`, `getExitCodeText` and `isSuccessful` as they were explicitly tested to return null in case the process is not started or terminated. I think it would be a BC break to do that. Commits ------- 449fe01 [Process] Trow exceptions in case a Process method is supposed to be called after termination 0ae6858 [Process] fixed fatal errors in getOutput and getErrorOutput when process was not started
2 parents e4c4a7d + 449fe01 commit c57fbda

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

src/Symfony/Component/Process/Process.php

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,12 @@ public function restart($callback = null)
300300
*
301301
* @throws RuntimeException When process timed out
302302
* @throws RuntimeException When process stopped after receiving signal
303+
* @throws LogicException When process is not yet started
303304
*/
304305
public function wait($callback = null)
305306
{
307+
$this->requireProcessIsStarted(__FUNCTION__);
308+
306309
$this->updateStatus(false);
307310
if (null !== $callback) {
308311
$this->callback = $this->buildCallback($callback);
@@ -370,10 +373,14 @@ public function signal($signal)
370373
*
371374
* @return string The process output
372375
*
376+
* @throws LogicException In case the process is not started
377+
*
373378
* @api
374379
*/
375380
public function getOutput()
376381
{
382+
$this->requireProcessIsStarted(__FUNCTION__);
383+
377384
$this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true);
378385

379386
return $this->stdout;
@@ -385,10 +392,14 @@ public function getOutput()
385392
* In comparison with the getOutput method which always return the whole
386393
* output, this one returns the new output since the last call.
387394
*
395+
* @throws LogicException In case the process is not started
396+
*
388397
* @return string The process output since the last call
389398
*/
390399
public function getIncrementalOutput()
391400
{
401+
$this->requireProcessIsStarted(__FUNCTION__);
402+
392403
$data = $this->getOutput();
393404

394405
$latest = substr($data, $this->incrementalOutputOffset);
@@ -402,10 +413,14 @@ public function getIncrementalOutput()
402413
*
403414
* @return string The process error output
404415
*
416+
* @throws LogicException In case the process is not started
417+
*
405418
* @api
406419
*/
407420
public function getErrorOutput()
408421
{
422+
$this->requireProcessIsStarted(__FUNCTION__);
423+
409424
$this->readPipes(false, defined('PHP_WINDOWS_VERSION_BUILD') ? !$this->processInformation['running'] : true);
410425

411426
return $this->stderr;
@@ -418,10 +433,14 @@ public function getErrorOutput()
418433
* whole error output, this one returns the new error output since the last
419434
* call.
420435
*
436+
* @throws LogicException In case the process is not started
437+
*
421438
* @return string The process error output since the last call
422439
*/
423440
public function getIncrementalErrorOutput()
424441
{
442+
$this->requireProcessIsStarted(__FUNCTION__);
443+
425444
$data = $this->getErrorOutput();
426445

427446
$latest = substr($data, $this->incrementalErrorOutputOffset);
@@ -433,7 +452,7 @@ public function getIncrementalErrorOutput()
433452
/**
434453
* Returns the exit code returned by the process.
435454
*
436-
* @return integer The exit status code
455+
* @return null|integer The exit status code, null if the Process is not terminated
437456
*
438457
* @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
439458
*
@@ -456,14 +475,18 @@ public function getExitCode()
456475
* This method relies on the Unix exit code status standardization
457476
* and might not be relevant for other operating systems.
458477
*
459-
* @return string A string representation for the exit status code
478+
* @return null|string A string representation for the exit status code, null if the Process is not terminated.
479+
*
480+
* @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
460481
*
461482
* @see http://tldp.org/LDP/abs/html/exitcodes.html
462483
* @see http://en.wikipedia.org/wiki/Unix_signal
463484
*/
464485
public function getExitCodeText()
465486
{
466-
$exitcode = $this->getExitCode();
487+
if (null === $exitcode = $this->getExitCode()) {
488+
return;
489+
}
467490

468491
return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
469492
}
@@ -488,11 +511,14 @@ public function isSuccessful()
488511
* @return Boolean
489512
*
490513
* @throws RuntimeException In case --enable-sigchild is activated
514+
* @throws LogicException In case the process is not terminated.
491515
*
492516
* @api
493517
*/
494518
public function hasBeenSignaled()
495519
{
520+
$this->requireProcessIsTerminated(__FUNCTION__);
521+
496522
if ($this->isSigchildEnabled()) {
497523
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
498524
}
@@ -510,11 +536,14 @@ public function hasBeenSignaled()
510536
* @return integer
511537
*
512538
* @throws RuntimeException In case --enable-sigchild is activated
539+
* @throws LogicException In case the process is not terminated.
513540
*
514541
* @api
515542
*/
516543
public function getTermSignal()
517544
{
545+
$this->requireProcessIsTerminated(__FUNCTION__);
546+
518547
if ($this->isSigchildEnabled()) {
519548
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
520549
}
@@ -531,10 +560,14 @@ public function getTermSignal()
531560
*
532561
* @return Boolean
533562
*
563+
* @throws LogicException In case the process is not terminated.
564+
*
534565
* @api
535566
*/
536567
public function hasBeenStopped()
537568
{
569+
$this->requireProcessIsTerminated(__FUNCTION__);
570+
538571
$this->updateStatus(false);
539572

540573
return $this->processInformation['stopped'];
@@ -547,10 +580,14 @@ public function hasBeenStopped()
547580
*
548581
* @return integer
549582
*
583+
* @throws LogicException In case the process is not terminated.
584+
*
550585
* @api
551586
*/
552587
public function getStopSignal()
553588
{
589+
$this->requireProcessIsTerminated(__FUNCTION__);
590+
554591
$this->updateStatus(false);
555592

556593
return $this->processInformation['stopsig'];
@@ -938,6 +975,10 @@ public function setEnhanceSigchildCompatibility($enhance)
938975
*/
939976
public function checkTimeout()
940977
{
978+
if ($this->status !== self::STATUS_STARTED) {
979+
return;
980+
}
981+
941982
if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
942983
$this->stop(0);
943984

@@ -1155,4 +1196,32 @@ private function doSignal($signal, $throwException)
11551196

11561197
return true;
11571198
}
1199+
1200+
/**
1201+
* Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1202+
*
1203+
* @param $functionName The function name that was called.
1204+
*
1205+
* @throws LogicException If the process has not run.
1206+
*/
1207+
private function requireProcessIsStarted($functionName)
1208+
{
1209+
if (!$this->isStarted()) {
1210+
throw new LogicException(sprintf('Process must be started before calling %s.', $functionName));
1211+
}
1212+
}
1213+
1214+
/**
1215+
* Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`.
1216+
*
1217+
* @param $functionName The function name that was called.
1218+
*
1219+
* @throws LogicException If the process is not yet terminated.
1220+
*/
1221+
private function requireProcessIsTerminated($functionName)
1222+
{
1223+
if (!$this->isTerminated()) {
1224+
throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
1225+
}
1226+
}
11581227
}

src/Symfony/Component/Process/Tests/AbstractProcessTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@ public function testTTYCommandExitCode()
272272
$this->assertTrue($process->isSuccessful());
273273
}
274274

275+
public function testExitCodeTextIsNullWhenExitCodeIsNull()
276+
{
277+
$process = $this->getProcess('');
278+
$this->assertNull($process->getExitCodeText());
279+
}
280+
275281
public function testExitCodeText()
276282
{
277283
$process = $this->getProcess('');
@@ -512,6 +518,19 @@ public function testRunProcessWithTimeout()
512518
$this->assertLessThan($timeout + Process::TIMEOUT_PRECISION, $duration);
513519
}
514520

521+
public function testCheckTimeoutOnNonStartedProcess()
522+
{
523+
$process = $this->getProcess('php -r "sleep(3);"');
524+
$process->checkTimeout();
525+
}
526+
527+
public function testCheckTimeoutOnTerminatedProcess()
528+
{
529+
$process = $this->getProcess('php -v');
530+
$process->run();
531+
$process->checkTimeout();
532+
}
533+
515534
public function testCheckTimeoutOnStartedProcess()
516535
{
517536
$timeout = 0.5;
@@ -617,6 +636,55 @@ public function testSignalProcessNotRunning()
617636
$process->signal(SIGHUP);
618637
}
619638

639+
/**
640+
* @dataProvider provideMethodsThatNeedARunningProcess
641+
*/
642+
public function testMethodsThatNeedARunningProcess($method)
643+
{
644+
$process = $this->getProcess('php -m');
645+
$this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method));
646+
call_user_func(array($process, $method));
647+
}
648+
649+
public function provideMethodsThatNeedARunningProcess()
650+
{
651+
return array(
652+
array('getOutput'),
653+
array('getIncrementalOutput'),
654+
array('getErrorOutput'),
655+
array('getIncrementalErrorOutput'),
656+
array('wait'),
657+
);
658+
}
659+
660+
/**
661+
* @dataProvider provideMethodsThatNeedATerminatedProcess
662+
*/
663+
public function testMethodsThatNeedATerminatedProcess($method)
664+
{
665+
$process = $this->getProcess('php -r "sleep(1);"');
666+
$process->start();
667+
try {
668+
call_user_func(array($process, $method));
669+
$process->stop(0);
670+
$this->fail('A LogicException must have been thrown');
671+
} catch (\Exception $e) {
672+
$this->assertInstanceOf('Symfony\Component\Process\Exception\LogicException', $e);
673+
$this->assertEquals(sprintf('Process must be terminated before calling %s.', $method), $e->getMessage());
674+
}
675+
$process->stop(0);
676+
}
677+
678+
public function provideMethodsThatNeedATerminatedProcess()
679+
{
680+
return array(
681+
array('hasBeenSignaled'),
682+
array('getTermSignal'),
683+
array('hasBeenStopped'),
684+
array('getStopSignal'),
685+
);
686+
}
687+
620688
private function verifyPosixIsEnabled()
621689
{
622690
if (defined('PHP_WINDOWS_VERSION_BUILD')) {

0 commit comments

Comments
 (0)