diff --git a/bin/composer b/bin/composer index 0664e04ce..a884abbb2 100755 --- a/bin/composer +++ b/bin/composer @@ -7,16 +7,13 @@ if (PHP_SAPI !== 'cli') { require __DIR__.'/../src/bootstrap.php'; -use Composer\Factory; -use Composer\XdebugHandler; use Composer\Console\Application; +use Composer\XdebugHandler\XdebugHandler; error_reporting(-1); -// Create output for XdebugHandler and Application -$output = Factory::createOutput(); - -$xdebug = new XdebugHandler($output); +// Restart without xdebug +$xdebug = new XdebugHandler('Composer', '--ansi'); $xdebug->check(); unset($xdebug); @@ -56,4 +53,4 @@ putenv('COMPOSER_BINARY='.realpath($_SERVER['argv'][0])); // run the command application $application = new Application(); -$application->run(null, $output); +$application->run(); diff --git a/composer.json b/composer.json index dd672c3b7..bbeb02c42 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "composer/ca-bundle": "^1.0", "composer/semver": "^1.0", "composer/spdx-licenses": "^1.2", + "composer/xdebug-handler": "^1.0", "seld/jsonlint": "^1.4", "symfony/console": "^2.7 || ^3.0 || ^4.0", "symfony/finder": "^2.7 || ^3.0 || ^4.0", diff --git a/composer.lock b/composer.lock index 771fca79c..ea095dbc6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "a248442611fb58177b28432be1af692c", + "content-hash": "6f0e4eb8b744ac97ff62245bed9d8aa5", "packages": [ { "name": "composer/ca-bundle", @@ -185,6 +185,50 @@ ], "time": "2018-01-31T13:17:27+00:00" }, + { + "name": "composer/xdebug-handler", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "1852e549ad0d6f2910f89c27fec833dda1b25b4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1852e549ad0d6f2910f89c27fec833dda1b25b4a", + "reference": "1852e549ad0d6f2910f89c27fec833dda1b25b4a", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "time": "2018-03-08T13:09:50+00:00" + }, { "name": "justinrainbow/json-schema", "version": "5.2.6", diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index 34eca8be2..3fdbcd867 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -122,6 +122,7 @@ class Compiler ->in(__DIR__.'/../../vendor/composer/spdx-licenses/') ->in(__DIR__.'/../../vendor/composer/semver/') ->in(__DIR__.'/../../vendor/composer/ca-bundle/') + ->in(__DIR__.'/../../vendor/composer/xdebug-handler/') ->in(__DIR__.'/../../vendor/psr/') ->sort($finderSort) ; diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index da5c9ab73..02d552424 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -12,12 +12,12 @@ namespace Composer\Repository; -use Composer\XdebugHandler; use Composer\Package\CompletePackage; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Plugin\PluginInterface; use Composer\Util\Silencer; +use Composer\XdebugHandler\XdebugHandler; /** * @author Jordi Boggiano @@ -120,7 +120,7 @@ class PlatformRepository extends ArrayRepository } // Check for xdebug in a restarted process - if (!in_array('xdebug', $loadedExtensions, true) && ($prettyVersion = strval(getenv(XdebugHandler::ENV_VERSION)))) { + if (!in_array('xdebug', $loadedExtensions, true) && ($prettyVersion = XdebugHandler::getSkippedVersion())) { $this->addExtension('xdebug', $prettyVersion); } diff --git a/src/Composer/Util/IniHelper.php b/src/Composer/Util/IniHelper.php index de1065320..d655419fc 100644 --- a/src/Composer/Util/IniHelper.php +++ b/src/Composer/Util/IniHelper.php @@ -12,6 +12,8 @@ namespace Composer\Util; +use Composer\XdebugHandler\XdebugHandler; + /** * Provides ini file location functions that work with and without a restart. * When the process has restarted it uses a tmp ini and stores the original @@ -21,8 +23,6 @@ namespace Composer\Util; */ class IniHelper { - const ENV_ORIGINAL = 'COMPOSER_ORIGINAL_INIS'; - /** * Returns an array of php.ini locations with at least one entry * @@ -33,19 +33,7 @@ class IniHelper */ public static function getAll() { - $env = getenv(self::ENV_ORIGINAL); - - if (false !== $env) { - return explode(PATH_SEPARATOR, $env); - } - - $paths = array(strval(php_ini_loaded_file())); - - if ($scanned = php_ini_scanned_files()) { - $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); - } - - return $paths; + return XdebugHandler::getAllIniFiles(); } /** diff --git a/src/Composer/XdebugHandler.php b/src/Composer/XdebugHandler.php index 7031e6ff8..015ac808a 100644 --- a/src/Composer/XdebugHandler.php +++ b/src/Composer/XdebugHandler.php @@ -12,290 +12,11 @@ namespace Composer; -use Composer\Util\IniHelper; -use Symfony\Component\Console\Output\OutputInterface; +trigger_error('The ' . __NAMESPACE__ . '\XdebugHandler class is deprecated, use Composer\XdebugHandler\XdebugHandler instead.', E_USER_DEPRECATED); /** - * @author John Stevenson + * @deprecated use Composer\XdebugHandler\XdebugHandler instead */ class XdebugHandler { - const ENV_ALLOW = 'COMPOSER_ALLOW_XDEBUG'; - const ENV_VERSION = 'COMPOSER_XDEBUG_VERSION'; - const RESTART_ID = 'internal'; - - private $output; - private $loaded; - private $envScanDir; - private $version; - private $tmpIni; - - /** - * Constructor - */ - public function __construct(OutputInterface $output) - { - $this->output = $output; - $this->loaded = extension_loaded('xdebug'); - $this->envScanDir = getenv('PHP_INI_SCAN_DIR'); - - if ($this->loaded) { - $ext = new \ReflectionExtension('xdebug'); - $this->version = strval($ext->getVersion()); - } - } - - /** - * Checks if xdebug is loaded and composer needs to be restarted - * - * If so, then a tmp ini is created with the xdebug ini entry commented out. - * If additional inis have been loaded, these are combined into the tmp ini - * and PHP_INI_SCAN_DIR is set to an empty value. Current ini locations are - * are stored in COMPOSER_ORIGINAL_INIS, for use in the restarted process. - * - * This behaviour can be disabled by setting the COMPOSER_ALLOW_XDEBUG - * environment variable to 1. This variable is used internally so that the - * restarted process is created only once and PHP_INI_SCAN_DIR can be - * restored to its original value. - */ - public function check() - { - $args = explode('|', strval(getenv(self::ENV_ALLOW)), 2); - - if ($this->needsRestart($args[0])) { - if ($this->prepareRestart()) { - $command = $this->getCommand(); - $this->restart($command); - } - - return; - } - - // Restore environment variables if we are restarting - if (self::RESTART_ID === $args[0]) { - putenv(self::ENV_ALLOW); - - if (false !== $this->envScanDir) { - // $args[1] contains the original value - if (isset($args[1])) { - putenv('PHP_INI_SCAN_DIR='.$args[1]); - } else { - putenv('PHP_INI_SCAN_DIR'); - } - } - - // Clear version if the restart failed to disable xdebug - if ($this->loaded) { - putenv(self::ENV_VERSION); - } - } - } - - /** - * Executes the restarted command then deletes the tmp ini - * - * @param string $command - */ - protected function restart($command) - { - passthru($command, $exitCode); - - if (!empty($this->tmpIni)) { - @unlink($this->tmpIni); - } - - exit($exitCode); - } - - /** - * Returns true if a restart is needed - * - * @param string $allow Environment value - * - * @return bool - */ - private function needsRestart($allow) - { - if (PHP_SAPI !== 'cli' || !defined('PHP_BINARY')) { - return false; - } - - return empty($allow) && $this->loaded; - } - - /** - * Returns true if everything was written for the restart - * - * If any of the following fails (however unlikely) we must return false to - * stop potential recursion: - * - tmp ini file creation - * - environment variable creation - * - * @return bool - */ - private function prepareRestart() - { - $this->tmpIni = ''; - $iniPaths = IniHelper::getAll(); - $additional = count($iniPaths) > 1; - - if ($this->writeTmpIni($iniPaths)) { - return $this->setEnvironment($additional, $iniPaths); - } - - return false; - } - - /** - * Returns true if the tmp ini file was written - * - * The filename is passed as the -c option when the process restarts. - * - * @param array $iniPaths Locations reported by the current process - * - * @return bool - */ - private function writeTmpIni(array $iniPaths) - { - if (!$this->tmpIni = tempnam(sys_get_temp_dir(), '')) { - return false; - } - - // $iniPaths has at least one item and it may be empty - if (empty($iniPaths[0])) { - array_shift($iniPaths); - } - - $content = ''; - $regex = '/^\s*(zend_extension\s*=.*xdebug.*)$/mi'; - - foreach ($iniPaths as $file) { - $data = preg_replace($regex, ';$1', file_get_contents($file)); - $content .= $data.PHP_EOL; - } - - $content .= 'allow_url_fopen='.ini_get('allow_url_fopen').PHP_EOL; - $content .= 'disable_functions="'.ini_get('disable_functions').'"'.PHP_EOL; - $content .= 'memory_limit='.ini_get('memory_limit').PHP_EOL; - - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - // Work-around for PHP windows bug, see issue #6052 - $content .= 'opcache.enable_cli=0'.PHP_EOL; - } - - return @file_put_contents($this->tmpIni, $content); - } - - /** - * Returns the restart command line - * - * @return string - */ - private function getCommand() - { - $phpArgs = array(PHP_BINARY, '-c', $this->tmpIni); - $params = array_merge($phpArgs, $this->getScriptArgs($_SERVER['argv'])); - - return implode(' ', array_map(array($this, 'escape'), $params)); - } - - /** - * Returns true if the restart environment variables were set - * - * @param bool $additional Whether there were additional inis - * @param array $iniPaths Locations reported by the current process - * - * @return bool - */ - private function setEnvironment($additional, array $iniPaths) - { - // Set scan dir to an empty value if additional ini files were used - if ($additional && !putenv('PHP_INI_SCAN_DIR=')) { - return false; - } - - // Make original inis available to restarted process - if (!putenv(IniHelper::ENV_ORIGINAL.'='.implode(PATH_SEPARATOR, $iniPaths))) { - return false; - } - - // Make xdebug version available to restarted process - if (!putenv(self::ENV_VERSION.'='.$this->version)) { - return false; - } - - // Flag restarted process and save env scan dir state - $args = array(self::RESTART_ID); - - if (false !== $this->envScanDir) { - // Save current PHP_INI_SCAN_DIR - $args[] = $this->envScanDir; - } - - return putenv(self::ENV_ALLOW.'='.implode('|', $args)); - } - - /** - * Returns the restart script arguments, adding --ansi if required - * - * If we are a terminal with color support we must ensure that the --ansi - * option is set, because the restarted output is piped. - * - * @param array $args The argv array - * - * @return array - */ - private function getScriptArgs(array $args) - { - if (in_array('--no-ansi', $args) || in_array('--ansi', $args)) { - return $args; - } - - if ($this->output->isDecorated()) { - $offset = count($args) > 1 ? 2 : 1; - array_splice($args, $offset, 0, '--ansi'); - } - - return $args; - } - - /** - * Escapes a string to be used as a shell argument. - * - * From https://github.com/johnstevenson/winbox-args - * MIT Licensed (c) John Stevenson - * - * @param string $arg The argument to be escaped - * @param bool $meta Additionally escape cmd.exe meta characters - * - * @return string The escaped argument - */ - private function escape($arg, $meta = true) - { - if (!defined('PHP_WINDOWS_VERSION_BUILD')) { - return escapeshellarg($arg); - } - - $quote = strpbrk($arg, " \t") !== false || $arg === ''; - $arg = preg_replace('/(\\\\*)"/', '$1$1\\"', $arg, -1, $dquotes); - - if ($meta) { - $meta = $dquotes || preg_match('/%[^%]+%/', $arg); - - if (!$meta && !$quote) { - $quote = strpbrk($arg, '^&|<>()') !== false; - } - } - - if ($quote) { - $arg = preg_replace('/(\\\\*)$/', '$1$1', $arg); - $arg = '"'.$arg.'"'; - } - - if ($meta) { - $arg = preg_replace('/(["^&|<>()%])/', '^$1', $arg); - } - - return $arg; - } } diff --git a/tests/Composer/Test/Mock/XdebugHandlerMock.php b/tests/Composer/Test/Mock/XdebugHandlerMock.php deleted file mode 100644 index 499f63b09..000000000 --- a/tests/Composer/Test/Mock/XdebugHandlerMock.php +++ /dev/null @@ -1,48 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Test\Mock; - -use Composer\Factory; -use Composer\XdebugHandler; - -class XdebugHandlerMock extends XdebugHandler -{ - public $restarted; - public $output; - public $testVersion = '2.5.0'; - - public function __construct($loaded = null) - { - $this->output = Factory::createOutput(); - parent::__construct($this->output); - - $loaded = null === $loaded ? true : $loaded; - $class = new \ReflectionClass(get_parent_class($this)); - - $prop = $class->getProperty('loaded'); - $prop->setAccessible(true); - $prop->setValue($this, $loaded); - - $prop = $class->getProperty('version'); - $prop->setAccessible(true); - $version = $loaded ? $this->testVersion : ''; - $prop->setValue($this, $version); - - $this->restarted = false; - } - - protected function restart($command) - { - $this->restarted = true; - } -} diff --git a/tests/Composer/Test/Util/IniHelperTest.php b/tests/Composer/Test/Util/IniHelperTest.php index be59d546e..aead5fea9 100644 --- a/tests/Composer/Test/Util/IniHelperTest.php +++ b/tests/Composer/Test/Util/IniHelperTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Util; use Composer\Util\IniHelper; +use Composer\XdebugHandler\XdebugHandler; use PHPUnit\Framework\TestCase; /** @@ -41,7 +42,6 @@ class IniHelperTest extends TestCase $this->setEnv($paths); $this->assertContains('loaded.ini', IniHelper::getMessage()); - $this->assertEquals($paths, IniHelper::getAll()); } public function testWithLoadedIniAndAdditional() @@ -72,22 +72,24 @@ class IniHelperTest extends TestCase public static function setUpBeforeClass() { + // Register our name with XdebugHandler + $xdebug = new XdebugHandler('composer'); // Save current state - self::$envOriginal = getenv(IniHelper::ENV_ORIGINAL); + self::$envOriginal = getenv('COMPOSER_ORIGINAL_INIS'); } public static function tearDownAfterClass() { // Restore original state if (false !== self::$envOriginal) { - putenv(IniHelper::ENV_ORIGINAL.'='.self::$envOriginal); + putenv('COMPOSER_ORIGINAL_INIS='.self::$envOriginal); } else { - putenv(IniHelper::ENV_ORIGINAL); + putenv('COMPOSER_ORIGINAL_INIS'); } } protected function setEnv(array $paths) { - putenv(IniHelper::ENV_ORIGINAL.'='.implode(PATH_SEPARATOR, $paths)); + putenv('COMPOSER_ORIGINAL_INIS='.implode(PATH_SEPARATOR, $paths)); } } diff --git a/tests/Composer/Test/XdebugHandlerTest.php b/tests/Composer/Test/XdebugHandlerTest.php deleted file mode 100644 index 6ccefad5c..000000000 --- a/tests/Composer/Test/XdebugHandlerTest.php +++ /dev/null @@ -1,182 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Test; - -use Composer\Test\Mock\XdebugHandlerMock; -use Composer\Util\IniHelper; -use PHPUnit\Framework\TestCase; - -/** - * @author John Stevenson - * - * We use PHP_BINARY which only became available in PHP 5.4 * - * @requires PHP 5.4 - */ -class XdebugHandlerTest extends TestCase -{ - public static $env = array(); - - public function testRestartWhenLoaded() - { - $loaded = true; - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertTrue($xdebug->restarted); - $this->assertInternalType('string', getenv(IniHelper::ENV_ORIGINAL)); - } - - public function testNoRestartWhenNotLoaded() - { - $loaded = false; - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertFalse($xdebug->restarted); - $this->assertFalse(getenv(IniHelper::ENV_ORIGINAL)); - } - - public function testNoRestartWhenLoadedAndAllowed() - { - $loaded = true; - putenv(XdebugHandlerMock::ENV_ALLOW.'=1'); - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertFalse($xdebug->restarted); - } - - public function testEnvAllow() - { - $loaded = true; - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $expected = XdebugHandlerMock::RESTART_ID; - $this->assertEquals($expected, getenv(XdebugHandlerMock::ENV_ALLOW)); - - // Mimic restart - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertFalse($xdebug->restarted); - $this->assertFalse(getenv(XdebugHandlerMock::ENV_ALLOW)); - } - - public function testEnvAllowWithScanDir() - { - $loaded = true; - $dir = '/some/where'; - putenv('PHP_INI_SCAN_DIR='.$dir); - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $expected = XdebugHandlerMock::RESTART_ID.'|'.$dir; - $this->assertEquals($expected, getenv(XdebugHandlerMock::ENV_ALLOW)); - - // Mimic setting scan dir and restart - putenv('PHP_INI_SCAN_DIR='); - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertEquals($dir, getenv('PHP_INI_SCAN_DIR')); - } - - public function testEnvAllowWithEmptyScanDir() - { - $loaded = true; - putenv('PHP_INI_SCAN_DIR='); - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $expected = XdebugHandlerMock::RESTART_ID.'|'; - $this->assertEquals($expected, getenv(XdebugHandlerMock::ENV_ALLOW)); - - // Unset scan dir and mimic restart - putenv('PHP_INI_SCAN_DIR'); - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertEquals('', getenv('PHP_INI_SCAN_DIR')); - } - - public function testEnvVersionWhenLoaded() - { - $loaded = true; - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertEquals($xdebug->testVersion, getenv(XdebugHandlerMock::ENV_VERSION)); - - // Mimic successful restart - $loaded = false; - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertEquals($xdebug->testVersion, getenv(XdebugHandlerMock::ENV_VERSION)); - } - - public function testEnvVersionWhenNotLoaded() - { - $loaded = false; - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertFalse(getenv(XdebugHandlerMock::ENV_VERSION)); - } - - public function testEnvVersionWhenRestartFails() - { - $loaded = true; - - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - - // Mimic failed restart - $xdebug = new XdebugHandlerMock($loaded); - $xdebug->check(); - $this->assertFalse(getenv(XdebugHandlerMock::ENV_VERSION)); - } - - public static function setUpBeforeClass() - { - // Save current state - $names = array( - XdebugHandlerMock::ENV_ALLOW, - XdebugHandlerMock::ENV_VERSION, - 'PHP_INI_SCAN_DIR', - IniHelper::ENV_ORIGINAL, - ); - - foreach ($names as $name) { - self::$env[$name] = getenv($name); - } - } - - public static function tearDownAfterClass() - { - // Restore original state - foreach (self::$env as $name => $value) { - if (false !== $value) { - putenv($name.'='.$value); - } else { - putenv($name); - } - } - } - - protected function setUp() - { - // Ensure env is unset - putenv(XdebugHandlerMock::ENV_ALLOW); - putenv(XdebugHandlerMock::ENV_VERSION); - putenv('PHP_INI_SCAN_DIR'); - putenv(IniHelper::ENV_ORIGINAL); - } -}