Repository: certbot/certbot

Issue:
Dialog error messages
I haven't taken the time to dig into this, but `letsencrypt` loses the content of dialog error messages. I caused an error by making my terminal too small to display the `dialog` widget (see #2812). When doing this, the output to my terminal was:

```
An unexpected error occurred:
DialogError
```

The contents of my logfile were:

```
2016-04-11 18:31:36,104:DEBUG:letsencrypt.main:Root logging level set at 30
2016-04-11 18:31:36,105:INFO:letsencrypt.main:Saving debug log to /var/log/letsencrypt/letsencrypt.log
2016-04-11 18:31:36,105:DEBUG:letsencrypt.main:letsencrypt version: 0.6.0.dev0
2016-04-11 18:31:36,105:DEBUG:letsencrypt.main:Arguments: []
2016-04-11 18:31:36,106:DEBUG:letsencrypt.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#nginx,PluginEntryPoint#standalone,PluginEntryPoint#manual,PluginEntryPoint#webroot,PluginEntryPoint#apache,PluginEntryPoint#null)
2016-04-11 18:31:36,109:DEBUG:letsencrypt.plugins.selection:Requested authenticator None and installer None
2016-04-11 18:31:36,430:DEBUG:letsencrypt.plugins.disco:No installation (PluginEntryPoint#nginx): 
Traceback (most recent call last):
  File "/root/letsencrypt/letsencrypt/plugins/disco.py", line 104, in prepare
    self._initialized.prepare()
  File "/root/letsencrypt/letsencrypt-nginx/letsencrypt_nginx/configurator.py", line 115, in prepare
    raise errors.NoInstallationError
NoInstallationError
2016-04-11 18:31:36,430:DEBUG:letsencrypt.plugins.selection:Single candidate plugin: * apache
Description: Apache Web Server - Alpha
Interfaces: IAuthenticator, IInstaller, IPlugin
Entry point: apache = letsencrypt_apache.configurator:ApacheConfigurator
Initialized: <letsencrypt_apache.configurator.ApacheConfigurator object at 0x7fc63206bc10>
Prep: True
2016-04-11 18:31:36,431:DEBUG:letsencrypt.plugins.selection:Selected authenticator <letsencrypt_apache.configurator.ApacheConfigurator object at 0x7fc63206bc10> and installer <letsencrypt_apache.configurator.ApacheConfigurator object at 0x7fc63206bc10>
2016-04-11 18:31:38,909:DEBUG:letsencrypt.main:Exiting abnormally:
Traceback (most recent call last):
  File "/root/letsencrypt/venv/bin/letsencrypt", line 9, in <module>
    load_entry_point('letsencrypt', 'console_scripts', 'letsencrypt')()
  File "/root/letsencrypt/letsencrypt/main.py", line 692, in main
    return config.func(config, plugins)
  File "/root/letsencrypt/letsencrypt/main.py", line 452, in run
    domains = _find_domains(config, installer)
  File "/root/letsencrypt/letsencrypt/main.py", line 261, in _find_domains
    domains = display_ops.choose_names(installer)
  File "/root/letsencrypt/letsencrypt/display/ops.py", line 100, in choose_names
    return _choose_names_manually()
  File "/root/letsencrypt/letsencrypt/display/ops.py", line 150, in _choose_names_manually
    cli_flag="--domains")
  File "/root/letsencrypt/letsencrypt/display/util.py", line 146, in input
    return self.dialog.inputbox(message, width=self.width, height=height)
  File "/root/letsencrypt/venv/local/lib/python2.7/site-packages/dialog.py", line 2715, in inputbox
    kwargs, strip_xdialog_newline=True, raw_help=True)
  File "/root/letsencrypt/venv/local/lib/python2.7/site-packages/dialog.py", line 1746, in _widget_with_string_output
    code, output = self._perform(args, **kwargs)
  File "/root/letsencrypt/venv/local/lib/python2.7/site-packages/dialog.py", line 1541, in _perform
    args_file)
  File "/root/letsencrypt/venv/local/lib/python2.7/site-packages/dialog.py", line 1502, in _handle_program_exit
    child_output_rfd)
  File "/root/letsencrypt/venv/local/lib/python2.7/site-packages/dialog.py", line 1454, in _wait_for_program_termination
    child_output.strip()))
DialogError
```



Discussion:
> 18:23 <+bmw> For #2813, look at _handle_exception in main.py
> 18:23 <+bmw> It's an ugly function, but the idea is basically to get the information out of an exception and log it, instead of letting every
>              exception propogate and be shown to the user.
> 18:24 <+bmw> Dialog does something weird with their exceptions and we lose the message attached to them for some reason.


Resolving patch:
diff --git a/certbot/main.py b/certbot/main.py
--- a/certbot/main.py
+++ b/certbot/main.py
@@ -1,6 +1,7 @@
 """Certbot main entry point."""
 from __future__ import print_function
 import atexit
+import dialog
 import errno
 import functools
 import logging.handlers
@@ -674,7 +675,10 @@ def _handle_exception(exc_type, exc_value, trace, config):
             # Here we're passing a client or ACME error out to the client at the shell
             # Tell the user a bit about what happened, without overwhelming
             # them with a full traceback
-            err = traceback.format_exception_only(exc_type, exc_value)[0]
+            if issubclass(exc_type, dialog.error):
+                err = exc_value.complete_message()
+            else:
+                err = traceback.format_exception_only(exc_type, exc_value)[0]
             # Typical error from the ACME module:
             # acme.messages.Error: urn:acme:error:malformed :: The request message was
             # malformed :: Error creating new registration :: Validation of contact


Test patch:
diff --git a/certbot/tests/cli_test.py b/certbot/tests/cli_test.py
--- a/certbot/tests/cli_test.py
+++ b/certbot/tests/cli_test.py
@@ -2,6 +2,7 @@
 from __future__ import print_function
 
 import argparse
+import dialog
 import functools
 import itertools
 import os
@@ -922,6 +923,13 @@ def test_handle_exception(self, mock_sys):
         mock_sys.exit.assert_called_with(''.join(
             traceback.format_exception_only(KeyboardInterrupt, interrupt)))
 
+        # Test dialog errors
+        exception = dialog.error(message="test message")
+        main._handle_exception(
+                dialog.DialogError, exc_value=exception, trace=None, config=None)
+        error_msg = mock_sys.exit.call_args_list[-1][0][0]
+        self.assertTrue("test message" in error_msg)
+
     def test_read_file(self):
         rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo'))
         self.assertRaises(