diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 34a1f91238..fe93ce9843 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -492,20 +492,24 @@ class Application */ public function findNamespace($namespace) { - $allNamespaces = array(); - foreach ($this->getNamespaces() as $n) { - $allNamespaces[$n] = explode(':', $n); - } - - $found = array(); + $allNamespaces = $this->getNamespaces(); + $found = ''; foreach (explode(':', $namespace) as $i => $part) { - $abbrevs = static::getAbbreviations(array_unique(array_values(array_filter(array_map(function ($p) use ($i) { return isset($p[$i]) ? $p[$i] : ''; }, $allNamespaces))))); + // select sub-namespaces matching the current namespace we found + $namespaces = array(); + foreach ($allNamespaces as $n) { + if ('' === $found || 0 === strpos($n, $found)) { + $namespaces[$n] = explode(':', $n); + } + } + + $abbrevs = static::getAbbreviations(array_unique(array_values(array_filter(array_map(function ($p) use ($i) { return isset($p[$i]) ? $p[$i] : ''; }, $namespaces))))); if (!isset($abbrevs[$part])) { $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); if (1 <= $i) { - $part = implode(':', $found).':'.$part; + $part = $found.':'.$part; } if ($alternatives = $this->findAlternativeNamespace($part, $abbrevs)) { @@ -521,14 +525,19 @@ class Application throw new \InvalidArgumentException($message); } + // there are multiple matches, but $part is an exact match of one of them so we select it + if (in_array($part, $abbrevs[$part])) { + $abbrevs[$part] = array($part); + } + if (count($abbrevs[$part]) > 1) { throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($abbrevs[$part]))); } - $found[] = $abbrevs[$part][0]; + $found .= $found ? ':' . $abbrevs[$part][0] : $abbrevs[$part][0]; } - return implode(':', $found); + return $found; } /** @@ -651,21 +660,12 @@ class Application { $abbrevs = array(); foreach ($names as $name) { - for ($len = strlen($name) - 1; $len > 0; --$len) { + for ($len = strlen($name); $len > 0; --$len) { $abbrev = substr($name, 0, $len); - if (!isset($abbrevs[$abbrev])) { - $abbrevs[$abbrev] = array($name); - } else { - $abbrevs[$abbrev][] = $name; - } + $abbrevs[$abbrev][] = $name; } } - // Non-abbreviations always get entered, even if they aren't unique - foreach ($names as $name) { - $abbrevs[$name] = array($name); - } - return $abbrevs; } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 23edd490b3..e106257a87 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -345,6 +345,16 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase } } + public function testFindNamespaceDoesNotFailOnDeepSimilarNamespaces() + { + $application = $this->getMock('Symfony\Component\Console\Application', array('getNamespaces')); + $application->expects($this->once()) + ->method('getNamespaces') + ->will($this->returnValue(array('foo:sublong', 'bar:sub'))); + + $this->assertEquals('foo:sublong', $application->findNamespace('f:sub')); + } + public function testSetCatchExceptions() { $application = $this->getMock('Symfony\Component\Console\Application', array('getTerminalWidth')); diff --git a/src/Symfony/Component/HttpKernel/Config/FileLocator.php b/src/Symfony/Component/HttpKernel/Config/FileLocator.php index d241b9da19..47b543c15e 100644 --- a/src/Symfony/Component/HttpKernel/Config/FileLocator.php +++ b/src/Symfony/Component/HttpKernel/Config/FileLocator.php @@ -28,14 +28,16 @@ class FileLocator extends BaseFileLocator * Constructor. * * @param KernelInterface $kernel A KernelInterface instance - * @param string $path The path the global resource directory - * @param string|array $paths A path or an array of paths where to look for resources + * @param null|string $path The path the global resource directory + * @param array $paths An array of paths where to look for resources */ public function __construct(KernelInterface $kernel, $path = null, array $paths = array()) { $this->kernel = $kernel; - $this->path = $path; - $paths[] = $path; + if (null !== $path) { + $this->path = $path; + $paths[] = $path; + } parent::__construct($paths); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Config/FileLocatorTest.php b/src/Symfony/Component/HttpKernel/Tests/Config/FileLocatorTest.php new file mode 100755 index 0000000000..be59486269 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Config/FileLocatorTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Config; + +use Symfony\Component\HttpKernel\Config\FileLocator; + +class FileLocatorTest extends \PHPUnit_Framework_TestCase +{ + public function testLocate() + { + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + $kernel + ->expects($this->atLeastOnce()) + ->method('locateResource') + ->with('@BundleName/some/path', null, true) + ->will($this->returnValue('/bundle-name/some/path')); + $locator = new FileLocator($kernel); + $this->assertEquals('/bundle-name/some/path', $locator->locate('@BundleName/some/path')); + + $kernel + ->expects($this->never()) + ->method('locateResource'); + $this->setExpectedException('LogicException'); + $locator->locate('/some/path'); + } + + public function testLocateWithGlobalResourcePath() + { + $kernel = $this->getMock('Symfony\Component\HttpKernel\KernelInterface'); + $kernel + ->expects($this->atLeastOnce()) + ->method('locateResource') + ->with('@BundleName/some/path', '/global/resource/path', false); + + $locator = new FileLocator($kernel, '/global/resource/path'); + $locator->locate('@BundleName/some/path', null, false); + } +} diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index e60373d57b..0cfd371f6e 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -35,10 +35,14 @@ class Process const STDOUT = 1; const STDERR = 2; + // Timeout Precision in seconds. + const TIMEOUT_PRECISION = 0.2; + private $commandline; private $cwd; private $env; private $stdin; + private $starttime; private $timeout; private $options; private $exitcode; @@ -228,6 +232,7 @@ class Process throw new RuntimeException('Process is already running'); } + $this->starttime = microtime(true); $this->stdout = ''; $this->stderr = ''; $this->incrementalOutputOffset = 0; @@ -304,7 +309,7 @@ class Process $w = $writePipes; $e = null; - $n = @stream_select($r, $w, $e, $this->timeout); + $n = @stream_select($r, $w, $e, 0, ceil(static::TIMEOUT_PRECISION * 1E6)); if (false === $n) { break; @@ -337,6 +342,8 @@ class Process unset($this->pipes[$type]); } } + + $this->checkTimeout(); } $this->updateStatus(); @@ -360,7 +367,7 @@ class Process public function restart($callback = null) { if ($this->isRunning()) { - throw new \RuntimeException('Process is already running'); + throw new RuntimeException('Process is already running'); } $process = clone $this; @@ -391,13 +398,15 @@ class Process if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles) { $this->processFileHandles($callback, !$this->pipes); } + $this->checkTimeout(); if ($this->pipes) { $r = $this->pipes; $w = null; $e = null; - if (false === $n = @stream_select($r, $w, $e, $this->timeout)) { + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, ceil(static::TIMEOUT_PRECISION * 1E6))) { $lastError = error_get_last(); // stream_select returns false when the `select` system call is interrupted by an incoming signal @@ -407,10 +416,10 @@ class Process continue; } - if (0 === $n) { - proc_terminate($this->process); - throw new RuntimeException('The process timed out.'); + // nothing has changed + if (0 === $n) { + continue; } foreach ($r as $pipe) { @@ -712,7 +721,7 @@ class Process */ public function stop($timeout = 10) { - $timeoutMicro = (int) $timeout*10E6; + $timeoutMicro = (int) $timeout*1E6; if ($this->isRunning()) { proc_terminate($this->process); $time = 0; @@ -721,6 +730,10 @@ class Process usleep(1000); } + if (!defined('PHP_WINDOWS_VERSION_BUILD') && $this->isRunning()) { + proc_terminate($this->process, SIGKILL); + } + foreach ($this->pipes as $pipe) { fclose($pipe); } @@ -800,7 +813,7 @@ class Process * * To disable the timeout, set this value to null. * - * @param integer|null $timeout The timeout in seconds + * @param float|null $timeout The timeout in seconds * * @return self The current Process instance * @@ -814,10 +827,10 @@ class Process return $this; } - $timeout = (integer) $timeout; + $timeout = (float) $timeout; if ($timeout < 0) { - throw new InvalidArgumentException('The timeout value must be a valid positive integer.'); + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } $this->timeout = $timeout; @@ -982,6 +995,24 @@ class Process return $this; } + /** + * Performs a check between the timeout definition and the time the process + * started + * + * In case you run a background process (with the start method), you should + * trigger this method regularly to ensure the process timeout + * + * @throws RuntimeException In case the timeout was reached + */ + public function checkTimeout() + { + if (0 < $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(0); + + throw new RuntimeException('The process timed-out.'); + } + } + /** * Builds up the callback used by wait(). * diff --git a/src/Symfony/Component/Process/ProcessBuilder.php b/src/Symfony/Component/Process/ProcessBuilder.php index 1a95bd0df4..c4ef31d311 100644 --- a/src/Symfony/Component/Process/ProcessBuilder.php +++ b/src/Symfony/Component/Process/ProcessBuilder.php @@ -103,7 +103,7 @@ class ProcessBuilder * * To disable the timeout, set this value to null. * - * @param integer|null + * @param float|null * * @return ProcessBuilder * @@ -117,10 +117,10 @@ class ProcessBuilder return $this; } - $timeout = (integer) $timeout; + $timeout = (float) $timeout; if ($timeout < 0) { - throw new InvalidArgumentException('The timeout value must be a valid positive integer.'); + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } $this->timeout = $timeout; diff --git a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php index 64798e34c8..6c56015c44 100644 --- a/src/Symfony/Component/Process/Tests/AbstractProcessTest.php +++ b/src/Symfony/Component/Process/Tests/AbstractProcessTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Process\Tests; use Symfony\Component\Process\Process; +use Symfony\Component\Process\Exception\RuntimeException; /** * @author Robert Schönthal @@ -44,6 +45,31 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase $this->assertNull($p->getTimeout()); } + public function testStopWithTimeoutIsActuallyWorking() + { + 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 + // command with exec + $p = $this->getProcess('exec php '.__DIR__.'/NonStopableProcess.php 3'); + $p->start(); + usleep(100000); + $start = microtime(true); + $p->stop(1.1); + while ($p->isRunning()) { + usleep(1000); + } + $duration = microtime(true) - $start; + + $this->assertLessThan(1.3, $duration); + } + /** * tests results from sub processes * @@ -320,6 +346,47 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase // PHP will deadlock when it tries to cleanup $process } + public function testRunProcessWithTimeout() + { + $timeout = 0.5; + $process = $this->getProcess('sleep 3'); + $process->setTimeout($timeout); + $start = microtime(true); + try { + $process->run(); + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + + } + $duration = microtime(true) - $start; + + $this->assertLessThan($timeout + Process::TIMEOUT_PRECISION, $duration); + } + + public function testCheckTimeoutOnStartedProcess() + { + $timeout = 0.5; + $precision = 100000; + $process = $this->getProcess('sleep 3'); + $process->setTimeout($timeout); + $start = microtime(true); + + $process->start(); + + try { + while ($process->isRunning()) { + $process->checkTimeout(); + usleep($precision); + } + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + + } + $duration = microtime(true) - $start; + + $this->assertLessThan($timeout + $precision, $duration); + } + public function responsesCodeProvider() { return array( diff --git a/src/Symfony/Component/Process/Tests/NonStopableProcess.php b/src/Symfony/Component/Process/Tests/NonStopableProcess.php new file mode 100644 index 0000000000..a4db838256 --- /dev/null +++ b/src/Symfony/Component/Process/Tests/NonStopableProcess.php @@ -0,0 +1,37 @@ + (microtime(true) - $start)) { + usleep(1000); +}