Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Diag command #1769

Merged
merged 2 commits into from

6 participants

@Seldaek
Owner

I wasted some time again helping someone figuring out that their oauth token wasn't valid anymore, and this isn't the only problem. Time and time again people ask for support and didn't do any due diligence. I'm not really blaming anyone though, most of the time they just don't know what to check for.

Anyway to help support efforts, and hopefully enable people to fix problems themselves more often, I thought scripting what is scriptable would be good. This runs for now like this:

$ composer diag
Checking platform settings: OK
Checking http connectivity: OK
Checking composer.json: OK
Checking github.com oauth access: OK
Checking composer version: OK

I'd welcome new ideas for automatic checks obviously, but that can be added later. Anyone think this is a bad idea?

@michelsalib

:+1: That's a very good idea. You might also add check for access to custom repositories (not hosted on github).I had a hard time once because I forgot to upload my public key to my ssh protected repository.

@Seldaek
Owner
@michelsalib
@igorw

Very good idea, I'd even support a flag to automatically (with users consent) pastebin the output + composer.json + lock file.

Nitpick, but I'd rename to diagnose. Lazy people can still write diag, since console don't care.

@stof stof commented on the diff
src/Composer/Command/DiagCommand.php
((150 lines not shown))
+ if ($e instanceof TransportException && $e->getCode() === 401) {
+ return '<warning>The oauth token for '.$domain.' seems invalid, run "composer config --global --unset github-oauth.'.$domain.'" to remove it</warning>';
+ }
+
+ 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 '<warning>Your are not running the latest version</warning>';
+ } else {
@stof
stof added a note

No need for else as the if returns

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@simensen

I like @igorw's idea to integrate with pastebin or gist. That would be super helpful.

It might also be interesting to output some of the more troublesome/important root package only settings like minimum-stability. I know these are probably going to be easy to spot if you are sending the composer.json to pastebin, but since JSON lets you order the keys however you like it is sometimes easy for people to miss.

@Seldaek
Owner

@igorw I agree it might be helpful, but might be a security concern too. We need a service that allows you to delete at least, and most don't allow that without auth AFAIK.

Anyway I'll rename and merge, then we can add this later maybe once we see if it's useful at all.

@simensen I'd add that as an additional flag / verbose mode that is made to share info about your setup then, because by default just showing it to the user won't inform them much I think if they're confused about the whole thing anyway.

@Seldaek Seldaek merged commit 7740196 into composer:master

1 check passed

Details default The Travis build passed
@stloyd stloyd commented on the diff
src/Composer/Command/DiagCommand.php
((111 lines not shown))
+ {
+ $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);
@stloyd
stloyd added a note

Maybe to reduce load time etc., use HEAD request and return this data in headers at packagist?

@Seldaek Owner
Seldaek added a note

I don't quite see how moving stuff from response body to response headers makes anything faster? Maybe I misunderstood you though. But anyway this code is really not performance critical.

@stof
stof added a note

@Seldaek a HEAD request would only transfer the headers, not the body. So it would be smaller. but in this case, it would be weird as you would need a dedicated url handled by Symfony whereas the current code requests a static file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
8 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`.
View
9 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.
View
303 src/Composer/Command/DiagCommand.php
@@ -0,0 +1,303 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * 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 <j.boggiano@seld.be>
+ */
+class DiagCommand extends Command
+{
+ protected $rfs;
+ protected $failures = 0;
+
+ protected function configure()
+ {
+ $this
+ ->setName('diag')
+ ->setDescription('Diagnoses the system to identify common errors.')
+ ->setHelp(<<<EOT
+The <info>diag</info> 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 . '</' . $style . '>';
+ }
+ }
+
+ 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);
@stloyd
stloyd added a note

Maybe to reduce load time etc., use HEAD request and return this data in headers at packagist?

@Seldaek Owner
Seldaek added a note

I don't quite see how moving stuff from response body to response headers makes anything faster? Maybe I misunderstood you though. But anyway this code is really not performance critical.

@stof
stof added a note

@Seldaek a HEAD request would only transfer the headers, not the body. So it would be smaller. but in this case, it would be weird as you would need a dedicated url handled by Symfony whereas the current code requests a static file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ $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 '<warning>The oauth token for '.$domain.' seems invalid, run "composer config --global --unset github-oauth.'.$domain.'" to remove it</warning>';
+ }
+
+ 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 '<warning>Your are not running the latest version</warning>';
+ } else {
@stof
stof added a note

No need for else as the if returns

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ return true;
+ }
+ }
+
+ private function outputResult(OutputInterface $output, $result)
+ {
+ if (true === $result) {
+ $output->writeln('<info>OK</info>');
+ } else {
+ $this->failures++;
+ $output->writeln('<error>FAIL</error>');
+ 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.'</'.$style.'>';
+ };
+
+ // 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(?: *</td><td class="v">| *=> *)(.*?)(?:</td>|$)}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;
+ }
+}
View
1  src/Composer/Console/Application.php
@@ -199,6 +199,7 @@ protected function getDefaultCommands()
$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();
Something went wrong with that request. Please try again.