From 1060d015fb9fb88b19cd83bde638d480d2b36551 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 4 Apr 2013 00:43:08 +0200 Subject: [PATCH 1/3] Add composer diag command to diagnose problems automatically --- src/Composer/Command/DiagCommand.php | 303 +++++++++++++++++++++++++++ src/Composer/Console/Application.php | 1 + 2 files changed, 304 insertions(+) create mode 100644 src/Composer/Command/DiagCommand.php diff --git a/src/Composer/Command/DiagCommand.php b/src/Composer/Command/DiagCommand.php new file mode 100644 index 000000000..c0e0ff364 --- /dev/null +++ b/src/Composer/Command/DiagCommand.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 DiagCommand extends Command +{ + protected $rfs; + protected $failures = 0; + + protected function configure() + { + $this + ->setName('diag') + ->setDescription('Diagnoses the system to identify common errors.') + ->setHelp(<<diag 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'; + } else { + 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..d90fdec6c 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\DiagCommand(); if ('phar:' === substr(__FILE__, 0, 5)) { $commands[] = new Command\SelfUpdateCommand(); From 7740196b1382d244e4c01031d6418dd9c7a9ad08 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 4 Apr 2013 00:47:03 +0200 Subject: [PATCH 2/3] Add docs for diag command --- doc/03-cli.md | 8 ++++++++ doc/articles/troubleshooting.md | 9 ++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index de72f0136..69852fc1d 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. +## diag + +If you think you found a bug, or something is behaving strangely, you might +want to run the `diag` command to perform automated checks for many common +problems. + + $ php composer.phar diag + ## 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. From 605cd3ddc3e821a018cd9a7677cec41ed37f2b8a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 4 Apr 2013 15:44:18 +0200 Subject: [PATCH 3/3] Rename diag to diagnose, fix feedback --- doc/03-cli.md | 6 +++--- .../Command/{DiagCommand.php => DiagnoseCommand.php} | 10 +++++----- src/Composer/Console/Application.php | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/Composer/Command/{DiagCommand.php => DiagnoseCommand.php} (98%) diff --git a/doc/03-cli.md b/doc/03-cli.md index 69852fc1d..19d26939e 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -348,13 +348,13 @@ 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. -## diag +## diagnose If you think you found a bug, or something is behaving strangely, you might -want to run the `diag` command to perform automated checks for many common +want to run the `diagnose` command to perform automated checks for many common problems. - $ php composer.phar diag + $ php composer.phar diagnose ## help diff --git a/src/Composer/Command/DiagCommand.php b/src/Composer/Command/DiagnoseCommand.php similarity index 98% rename from src/Composer/Command/DiagCommand.php rename to src/Composer/Command/DiagnoseCommand.php index c0e0ff364..351cb22b1 100644 --- a/src/Composer/Command/DiagCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -24,7 +24,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano */ -class DiagCommand extends Command +class DiagnoseCommand extends Command { protected $rfs; protected $failures = 0; @@ -32,10 +32,10 @@ class DiagCommand extends Command protected function configure() { $this - ->setName('diag') + ->setName('diagnose') ->setDescription('Diagnoses the system to identify common errors.') ->setHelp(<<diag command checks common errors to help debugging problems. +The diagnose command checks common errors to help debugging problems. EOT ) @@ -162,9 +162,9 @@ EOT if (Composer::VERSION !== $latest && Composer::VERSION !== '@package_version@') { return 'Your are not running the latest version'; - } else { - return true; } + + return true; } private function outputResult(OutputInterface $output, $result) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index d90fdec6c..e2ba74a4c 100755 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -199,7 +199,7 @@ class Application extends BaseApplication $commands[] = new Command\DumpAutoloadCommand(); $commands[] = new Command\StatusCommand(); $commands[] = new Command\ArchiveCommand(); - $commands[] = new Command\DiagCommand(); + $commands[] = new Command\DiagnoseCommand(); if ('phar:' === substr(__FILE__, 0, 5)) { $commands[] = new Command\SelfUpdateCommand();