diff --git a/src/Symfony/Component/Process/CHANGELOG.md b/src/Symfony/Component/Process/CHANGELOG.md index 3d13b99cc01b3..3a18983865cb3 100644 --- a/src/Symfony/Component/Process/CHANGELOG.md +++ b/src/Symfony/Component/Process/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG ----- * added the ability to define an idle timeout + * added support for PTY mode + * added the convenience method "mustRun" 2.3.0 ----- diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index b8179f9144580..d9441e404a903 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -13,6 +13,7 @@ use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; @@ -62,6 +63,7 @@ class Process private $incrementalOutputOffset; private $incrementalErrorOutputOffset; private $tty; + private $pty; private $fileHandles; private $readBytes; @@ -118,6 +120,33 @@ class Process 159 => 'Bad syscall', ); + /** + * Returns whether PTY is supported on the current operating system. + * + * @return Boolean + */ + public static function isPtySupported() + { + static $result; + + if (null !== $result) { + return $result; + } + + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + return $result = false; + } + + $proc = @proc_open('echo 1', array(array('pty'), array('pty'), array('pty')), $pipes); + if (is_resource($proc)) { + proc_close($proc); + + return $result = true; + } + + return $result = false; + } + /** * Constructor. * @@ -156,6 +185,7 @@ public function __construct($commandline, $cwd = null, array $env = null, $stdin } $this->stdin = $stdin; $this->setTimeout($timeout); + $this->pty = false; $this->enhanceWindowsCompatibility = true; $this->enhanceSigchildCompatibility = !defined('PHP_WINDOWS_VERSION_BUILD') && $this->isSigchildEnabled(); $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); @@ -207,6 +237,25 @@ public function run($callback = null) return $this->wait($callback); } + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @param callable|null $callback + * + * @return self + */ + public function mustRun($callback = null) + { + if (0 !== $this->run($callback)) { + throw new ProcessFailedException($this); + } + + return $this; + } + /** * Starts the process and returns after sending the STDIN. * @@ -915,6 +964,30 @@ public function isTty() return $this->tty; } + /** + * Sets PTY mode. + * + * @param Boolean $bool + * + * @return self + */ + public function setPty($bool) + { + $this->pty = (Boolean) $bool; + + return $this; + } + + /** + * Returns PTY state. + * + * @return Boolean + */ + public function isPty() + { + return $this->pty; + } + /** * Gets the working directory. * @@ -1137,6 +1210,12 @@ private function getDescriptors() array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w'), ); + } elseif ($this->pty && self::isPtySupported()) { + $descriptors = array( + array('pty'), + array('pty'), + array('pty'), + ); } else { $descriptors = array( array('pipe', 'r'), // stdin diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index 60702553ba255..6a8b4b70b0c4b 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -200,6 +200,45 @@ public function testTTYCommand() $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); } + /** + * @group pty + */ + public function testPTYCommand() + { + if ( ! Process::isPtySupported()) { + $this->markTestSkipped('PTY is not supported on this operating system.'); + } + + $process = $this->getProcess('echo "foo"'); + $process->setPty(true); + $process->run(); + + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + $this->assertEquals("foo\r\n", $process->getOutput()); + } + + /** + * @group mustRun + */ + public function testMustRun() + { + $process = $this->getProcess('echo "foo"'); + + $this->assertSame($process, $process->mustRun()); + $this->assertEquals("foo\n", $process->getOutput()); + $this->assertEquals(0, $process->getExitCode()); + } + + /** + * @expectedException Symfony\Component\Process\Exception\ProcessFailedException + * @group mustRun + */ + public function testMustRunThrowsException() + { + $process = $this->getProcess('exit 1'); + $process->mustRun(); + } + public function testExitCodeText() { $process = $this->getProcess(''); diff --git a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php index 2e364d6392b97..2743f29052697 100644 --- a/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php +++ b/src/Symfony/Component/Process/Tests/SigchildDisabledProcessTest.php @@ -29,6 +29,24 @@ public function testExitCodeCommandFailed() parent::testExitCodeCommandFailed(); } + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @group mustRun + */ + public function testMustRun() + { + parent::testMustRun(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @group mustRun + */ + public function testMustRunThrowsException() + { + parent::testMustRunThrowsException(); + } + /** * @expectedException \Symfony\Component\Process\Exception\RuntimeException */