diff --git a/doc/03-cli.md b/doc/03-cli.md index de72f0136..19d26939e 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -348,6 +348,14 @@ performance. autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. +## diagnose + +If you think you found a bug, or something is behaving strangely, you might +want to run the `diagnose` command to perform automated checks for many common +problems. + + $ php composer.phar diagnose + ## help To get more information about a certain command, just use `help`. diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index 1282db7b9..3362471eb 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -7,13 +7,16 @@ This is a list of common pitfalls on using Composer, and how to avoid them. ## General -1. When facing any kind of problems using Composer, be sure to **work with the +1. Before asking anyone, run [`composer diag`](../03-cli.md#diag) to check + for common problems. If it all checks out, proceed to the next steps. + +2. When facing any kind of problems using Composer, be sure to **work with the latest version**. See [self-update](../03-cli.md#self-update) for details. -2. Make sure you have no problems with your setup by running the installer's +3. Make sure you have no problems with your setup by running the installer's checks via `curl -sS https://getcomposer.org/installer | php -- --check`. -3. Ensure you're **installing vendors straight from your `composer.json`** via +4. Ensure you're **installing vendors straight from your `composer.json`** via `rm -rf vendor && composer update -v` when troubleshooting, excluding any possible interferences with existing vendor installations or `composer.lock` entries. diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php new file mode 100644 index 000000000..351cb22b1 --- /dev/null +++ b/src/Composer/Command/DiagnoseCommand.php @@ -0,0 +1,303 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Composer\Composer; +use Composer\Factory; +use Composer\Downloader\TransportException; +use Composer\Util\ConfigValidator; +use Composer\Util\RemoteFilesystem; +use Composer\Util\StreamContextFactory; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jordi Boggiano + */ +class DiagnoseCommand extends Command +{ + protected $rfs; + protected $failures = 0; + + protected function configure() + { + $this + ->setName('diagnose') + ->setDescription('Diagnoses the system to identify common errors.') + ->setHelp(<<diagnose command checks common errors to help debugging problems. + +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->rfs = new RemoteFilesystem($this->getIO()); + + $output->write('Checking platform settings: '); + $this->outputResult($output, $this->checkPlatform()); + + $output->write('Checking http connectivity: '); + $this->outputResult($output, $this->checkHttp()); + + $opts = stream_context_get_options(StreamContextFactory::getContext()); + if (!empty($opts['http']['proxy'])) { + $output->write('Checking HTTP proxy: '); + $this->outputResult($output, $this->checkHttpProxy()); + } + + $composer = $this->getComposer(false); + if ($composer) { + $output->write('Checking composer.json: '); + $this->outputResult($output, $this->checkComposerSchema()); + } + + if ($composer) { + $config = $composer->getConfig(); + } else { + $config = Factory::createConfig(); + } + + if ($oauth = $config->get('github-oauth')) { + foreach ($oauth as $domain => $token) { + $output->write('Checking '.$domain.' oauth access: '); + $this->outputResult($output, $this->checkGithubOauth($domain, $token)); + } + } + + $output->write('Checking composer version: '); + $this->outputResult($output, $this->checkVersion()); + + return $this->failures; + } + + private function checkComposerSchema() + { + $validator = new ConfigValidator($this->getIO()); + list($errors, $publishErrors, $warnings) = $validator->validate(Factory::getComposerFile()); + + if ($errors || $publishErrors || $warnings) { + $messages = array( + 'error' => array_merge($errors, $publishErrors), + 'warning' => $warnings, + ); + + $output = ''; + foreach ($messages as $style => $msgs) { + foreach ($msgs as $msg) { + $output .= '<' . $style . '>' . $msg . ''; + } + } + + return $output; + } + + return true; + } + + private function checkHttp() + { + $protocol = extension_loaded('openssl') ? 'https' : 'http'; + try { + $json = $this->rfs->getContents('packagist.org', $protocol . '://packagist.org/packages.json', false); + } catch (\Exception $e) { + return $e; + } + + return true; + } + + private function checkHttpProxy() + { + $protocol = extension_loaded('openssl') ? 'https' : 'http'; + try { + $json = json_decode($this->rfs->getContents('packagist.org', $protocol . '://packagist.org/packages.json', false), true); + $hash = reset($json['provider-includes']); + $hash = $hash['sha256']; + $path = str_replace('%hash%', $hash, key($json['provider-includes'])); + $provider = $this->rfs->getContents('packagist.org', $protocol . '://packagist.org/'.$path, false); + + if (hash('sha256', $provider) !== $hash) { + return 'It seems that your proxy is modifying http traffic on the fly'; + } + } catch (\Exception $e) { + return $e; + } + + return true; + } + + private function checkGithubOauth($domain, $token) + { + $this->getIO()->setAuthentication($domain, $token, 'x-oauth-basic'); + try { + $url = $domain === 'github.com' ? 'https://api.'.$domain.'/user/repos' : 'https://'.$domain.'/api/v3/user/repos'; + + return $this->rfs->getContents($domain, $url, false) ? true : 'Unexpected error'; + } catch (\Exception $e) { + if ($e instanceof TransportException && $e->getCode() === 401) { + return 'The oauth token for '.$domain.' seems invalid, run "composer config --global --unset github-oauth.'.$domain.'" to remove it'; + } + + return $e; + } + } + + private function checkVersion() + { + $protocol = extension_loaded('openssl') ? 'https' : 'http'; + $latest = trim($this->rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/version', false)); + + if (Composer::VERSION !== $latest && Composer::VERSION !== '@package_version@') { + return 'Your are not running the latest version'; + } + + return true; + } + + private function outputResult(OutputInterface $output, $result) + { + if (true === $result) { + $output->writeln('OK'); + } else { + $this->failures++; + $output->writeln('FAIL'); + if ($result instanceof \Exception) { + $output->writeln('['.get_class($result).'] '.$result->getMessage()); + } elseif ($result) { + $output->writeln($result); + } + } + } + + private function checkPlatform() + { + $output = ''; + $out = function ($msg, $style) use (&$output) { + $output .= '<'.$style.'>'.$msg.''; + }; + + // code below taken from getcomposer.org/installer, any changes should be made there and replicated here + $errors = array(); + $warnings = array(); + + $iniPath = php_ini_loaded_file(); + $displayIniMessage = false; + if ($iniPath) { + $iniMessage = PHP_EOL.PHP_EOL.'The php.ini used by your command-line PHP is: ' . $iniPath; + } else { + $iniMessage = PHP_EOL.PHP_EOL.'A php.ini file does not exist. You will have to create one.'; + } + $iniMessage .= PHP_EOL.'If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.'; + + if (!ini_get('allow_url_fopen')) { + $errors['allow_url_fopen'] = true; + } + + if (version_compare(PHP_VERSION, '5.3.2', '<')) { + $errors['php'] = PHP_VERSION; + } + + if (version_compare(PHP_VERSION, '5.3.4', '<')) { + $warnings['php'] = PHP_VERSION; + } + + if (!extension_loaded('openssl')) { + $warnings['openssl'] = true; + } + + if (ini_get('apc.enable_cli')) { + $warnings['apc_cli'] = true; + } + + ob_start(); + phpinfo(INFO_GENERAL); + $phpinfo = ob_get_clean(); + if (preg_match('{Configure Command(?: *| *=> *)(.*?)(?:|$)}m', $phpinfo, $match)) { + $configure = $match[1]; + + if (false !== strpos($configure, '--enable-sigchild')) { + $warnings['sigchild'] = true; + } + + if (false !== strpos($configure, '--with-curlwrappers')) { + $warnings['curlwrappers'] = true; + } + } + + if (!empty($errors)) { + foreach ($errors as $error => $current) { + switch ($error) { + case 'php': + $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher."; + break; + + case 'allow_url_fopen': + $text = PHP_EOL."The allow_url_fopen setting is incorrect.".PHP_EOL; + $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; + $text .= " allow_url_fopen = On"; + $displayIniMessage = true; + break; + } + if ($displayIniMessage) { + $text .= $iniMessage; + } + $out($text, 'error'); + } + + $out(''); + } + + if (!empty($warnings)) { + foreach ($warnings as $warning => $current) { + switch ($warning) { + case 'apc_cli': + $text = PHP_EOL."The apc.enable_cli setting is incorrect.".PHP_EOL; + $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; + $text .= " apc.enable_cli = Off"; + $displayIniMessage = true; + break; + + case 'sigchild': + $text = PHP_EOL."PHP was compiled with --enable-sigchild which can cause issues on some platforms.".PHP_EOL; + $text .= "Recompile it without this flag if possible, see also:".PHP_EOL; + $text .= " https://bugs.php.net/bug.php?id=22999"; + break; + + case 'curlwrappers': + $text = PHP_EOL."PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.".PHP_EOL; + $text .= "Recompile it without this flag if possible"; + break; + + case 'openssl': + $text = PHP_EOL."The openssl extension is missing, which will reduce the security and stability of Composer.".PHP_EOL; + $text .= "If possible you should enable it or recompile php with --with-openssl"; + break; + + case 'php': + $text = PHP_EOL."Your PHP ({$current}) is quite old, upgrading to PHP 5.3.4 or higher is recommended.".PHP_EOL; + $text .= "Composer works with 5.3.2+ for most people, but there might be edge case issues."; + break; + } + if ($displayIniMessage) { + $text .= $iniMessage; + } + $out($text, 'warning'); + } + } + + return !$warnings && !$errors ? true : $output; + } +} diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 214b6ca5a..e2ba74a4c 100755 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -199,6 +199,7 @@ class Application extends BaseApplication $commands[] = new Command\DumpAutoloadCommand(); $commands[] = new Command\StatusCommand(); $commands[] = new Command\ArchiveCommand(); + $commands[] = new Command\DiagnoseCommand(); if ('phar:' === substr(__FILE__, 0, 5)) { $commands[] = new Command\SelfUpdateCommand();