diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 0cfd371f6e..3d3c95667c 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\RuntimeException; /** @@ -467,6 +468,51 @@ class Process return $this->exitcode; } + /** + * Returns the Pid (process identifier), if applicable. + * + * @return integer|null The process id if running, null otherwise + * + * @throws RuntimeException In case --enable-sigchild is activated + */ + public function getPid() + { + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); + } + + $this->updateStatus(); + + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * Sends a posix signal to the process. + * + * @param integer $signal A valid posix signal (see http://www.php.net/manual/en/pcntl.constants.php) + * @return Process + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated + * @throws RuntimeException In case of failure + */ + public function signal($signal) + { + if (!$this->isRunning()) { + throw new LogicException('Can not send signal on a non running process.'); + } + + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + if (true !== @proc_terminate($this->process, $signal)) { + throw new RuntimeException(sprintf('Error while sending signal `%d`.', $signal)); + } + + return $this; + } + /** * Returns the current output of the process (STDOUT). * @@ -714,12 +760,13 @@ class Process * Stops the process. * * @param integer|float $timeout The timeout in seconds + * @param integer $signal A posix signal to send in case the process has not stop at timeout, default is SIGKILL * * @return integer The exit-code of the process * * @throws RuntimeException if the process got signaled */ - public function stop($timeout = 10) + public function stop($timeout = 10, $signal = null) { $timeoutMicro = (int) $timeout*1E6; if ($this->isRunning()) { @@ -730,8 +777,10 @@ class Process usleep(1000); } - if (!defined('PHP_WINDOWS_VERSION_BUILD') && $this->isRunning()) { - proc_terminate($this->process, SIGKILL); + if ($this->isRunning() && !$this->isSigchildEnabled()) { + if (null !== $signal || defined('SIGKILL')) { + $this->signal($signal ?: SIGKILL); + } } foreach ($this->pipes as $pipe) { diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index ec63a39856..3d1bcf37e4 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -50,9 +50,6 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->markTestSkipped('Stop with timeout does not work on windows, it requires posix signals'); } - if (!function_exists('pcntl_signal')) { - $this->markTestSkipped('This test require pcntl_signal function'); - } // exec is mandatory here since we send a signal to the process // see https://github.com/symfony/symfony/issues/5030 about prepending @@ -61,7 +58,7 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $p->start(); usleep(100000); $start = microtime(true); - $p->stop(1.1); + $p->stop(1.1, SIGKILL); while ($p->isRunning()) { usleep(1000); } @@ -224,7 +221,7 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase public function testStatus() { - $process = $this->getProcess('php -r "sleep(1);"'); + $process = $this->getProcess('php -r "usleep(500000);"'); $this->assertFalse($process->isRunning()); $this->assertFalse($process->isStarted()); $this->assertFalse($process->isTerminated()); @@ -277,6 +274,17 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $this->assertFalse($process->hasBeenSignaled()); } + public function testProcessWithoutTermSignalIsNotSignaled() + { + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess('php -m'); + $process->run(); + $this->assertFalse($process->hasBeenSignaled()); + } + public function testProcessWithoutTermSignal() { if (defined('PHP_WINDOWS_VERSION_BUILD')) { @@ -387,6 +395,70 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $this->assertLessThan($timeout + $precision, $duration); } + public function testGetPid() + { + $process = $this->getProcess('php -r "sleep(1);"'); + $process->start(); + $this->assertGreaterThan(0, $process->getPid()); + $process->stop(); + } + + public function testGetPidIsNullBeforeStart() + { + $process = $this->getProcess('php -r "sleep(1);"'); + $this->assertNull($process->getPid()); + } + + public function testGetPidIsNullAfterRun() + { + $process = $this->getProcess('php -m'); + $process->run(); + $this->assertNull($process->getPid()); + } + + public function testSignal() + { + $process = $this->getProcess('exec php -f ' . __DIR__ . '/SignalListener.php'); + $process->start(); + usleep(500000); + $process->signal(SIGUSR1); + + while ($process->isRunning() && false === strpos($process->getoutput(), 'Caught SIGUSR1')) { + usleep(10000); + } + + $this->assertEquals('Caught SIGUSR1', $process->getOutput()); + } + + /** + * @expectedException Symfony\Component\Process\Exception\LogicException + */ + public function testSignalProcessNotRunning() + { + $process = $this->getProcess('php -m'); + $process->signal(SIGHUP); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongIntSignal() + { + $process = $this->getProcess('php -r "sleep(3);"'); + $process->start(); + $process->signal(-4); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongNonIntSignal() + { + $process = $this->getProcess('php -r "sleep(3);"'); + $process->start(); + $process->signal('Céphalopodes'); + } + public function responsesCodeProvider() { return array( diff --git a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php index 1e4dc1d0a2..37c348d35d 100644 --- a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php @@ -64,6 +64,30 @@ class SigchildDisabledProcessTest extends AbstractProcessTest /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */ + public function testGetPid() + { + parent::testGetPid(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testGetPidIsNullBeforeStart() + { + parent::testGetPidIsNullBeforeStart(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testGetPidIsNullAfterRun() + { + parent::testGetPidIsNullAfterRun(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ public function testExitCodeText() { $process = $this->getProcess('qdfsmfkqsdfmqmsd'); @@ -88,6 +112,27 @@ class SigchildDisabledProcessTest extends AbstractProcessTest parent::testIsNotSuccessful(); } + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignal() + { + parent::testSignal(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testProcessWithoutTermSignalIsNotSignaled() + { + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + $this->markTestSkipped('Stopping with signal is not supported in sigchild environment'); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php b/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php index 5c664e2423..deec3a6504 100644 --- a/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SigchildEnabledProcessTest.php @@ -45,6 +45,30 @@ class SigchildEnabledProcessTest extends AbstractProcessTest parent::testProcessWithoutTermSignal(); } + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testGetPid() + { + parent::testGetPid(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testGetPidIsNullBeforeStart() + { + parent::testGetPidIsNullBeforeStart(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testGetPidIsNullAfterRun() + { + parent::testGetPidIsNullAfterRun(); + } + public function testExitCodeText() { $process = $this->getProcess('qdfsmfkqsdfmqmsd'); @@ -53,6 +77,22 @@ class SigchildEnabledProcessTest extends AbstractProcessTest $this->assertInternalType('string', $process->getExitCodeText()); } + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignal() + { + parent::testSignal(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testProcessWithoutTermSignalIsNotSignaled() + { + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Process/Tests/SignalListener.php b/src/Symfony/Component/Process/Tests/SignalListener.php new file mode 100644 index 0000000000..8ba167c331 --- /dev/null +++ b/src/Symfony/Component/Process/Tests/SignalListener.php @@ -0,0 +1,16 @@ +skipIfPHPSigchild(); + parent::testGetPid(); + } + + public function testGetPidIsNullBeforeStart() + { + $this->skipIfPHPSigchild(); + parent::testGetPidIsNullBeforeStart(); + } + + public function testGetPidIsNullAfterRun() + { + $this->skipIfPHPSigchild(); + parent::testGetPidIsNullAfterRun(); + } + + public function testSignal() + { + $this->skipIfPHPSigchild(); + parent::testSignal(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\LogicException + */ + public function testSignalProcessNotRunning() + { + $this->skipIfPHPSigchild(); + parent::testSignalProcessNotRunning(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongIntSignal() + { + $this->skipIfPHPSigchild(); + parent::testSignalWithWrongIntSignal(); + } + + /** + * @expectedException Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongNonIntSignal() + { + $this->skipIfPHPSigchild(); + parent::testSignalWithWrongNonIntSignal(); + } + /** * {@inheritdoc} */