diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 118eb6e39..c6df1052c 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -98,8 +98,8 @@ def _find_domains(args, installer): domains = args.domains if not domains: - sys.exit("Please specify --domains, or --installer that will " - "help in domain names autodiscovery") + raise errors.Error("Please specify --domains, or --installer that " + "will help in domain names autodiscovery") return domains @@ -116,7 +116,8 @@ def _init_acme(config, acc, authenticator, installer): acme.register() except errors.LetsEncryptClientError as error: logger.debug(error) - sys.exit("Unable to register an account with ACME server") + raise errors.Error("Unable to register an account with ACME " + "server") return acme @@ -465,6 +466,9 @@ def create_parser(plugins, args): "testing", description="The following flags are meant for " "testing purposes only! Do NOT change them, unless you " "really know what you're doing!") + helpful.add( + "testing", "--debug", action="store_true", + help="Show tracebacks if the program exits abnormally") helpful.add( "testing", "--no-verify-ssl", action="store_true", help=config_help("no_verify_ssl"), @@ -614,13 +618,8 @@ def _setup_logging(args): logger.info("Saving debug log to %s", log_file_name) -def main(cli_args=sys.argv[1:]): - """Command line argument parsing and main script execution.""" - # note: arg parser internally handles --help (and exits afterwards) - plugins = plugins_disco.PluginsRegistry.find_all() - args = create_parser(plugins, cli_args).parse_args(cli_args) - config = configuration.NamespaceConfig(args) - +def main2(cli_args, args, config, plugins): + """Continued main script execution.""" # Displayer if args.text_mode: displayer = display_util.FileDisplay(sys.stdout) @@ -659,5 +658,33 @@ def main(cli_args=sys.argv[1:]): return args.func(args, config, plugins) +def main(cli_args=sys.argv[1:]): + """Command line argument parsing and main script execution.""" + # note: arg parser internally handles --help (and exits afterwards) + plugins = plugins_disco.PluginsRegistry.find_all() + args = create_parser(plugins, cli_args).parse_args(cli_args) + config = configuration.NamespaceConfig(args) + + def handle_exception_common(): + """Logs the exception and reraises it if in debug mode.""" + logger.debug("Exiting abnormally", exc_info=True) + if args.debug: + raise + + try: + return main2(cli_args, args, config, plugins) + except errors.Error as error: + handle_exception_common() + return error + except KeyboardInterrupt: + handle_exception_common() + # Ensures a new line is printed + return "" + except: # pylint: disable=bare-except + handle_exception_common() + return ("An unexpected error occured. Please see the logfiles in {0} " + "for more details.".format(args.logs_dir)) + + if __name__ == "__main__": sys.exit(main()) # pragma: no cover diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 1e8e84f6d..6b8f77c62 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -20,13 +20,16 @@ class CLITest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.tmp_dir) - def _call(self, args): + def _call(self, args, client_mock_attrs=None): from letsencrypt import cli args = ['--text', '--config-dir', self.config_dir, '--work-dir', self.work_dir, '--logs-dir', self.logs_dir] + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: + if client_mock_attrs: + # pylint: disable=star-args + client.configure_mock(**client_mock_attrs) ret = cli.main(args) return ret, stdout, stderr, client @@ -55,6 +58,24 @@ class CLITest(unittest.TestCase): for r in xrange(len(flags)))): self._call(['plugins',] + list(args)) + def test_exceptions(self): + from letsencrypt import errors + cmd_arg = ['config_changes'] + error = [errors.Error('problem')] + attrs = {'view_config_changes.side_effect' : error} + self.assertRaises( + errors.Error, self._call, ['--debug'] + cmd_arg, attrs) + self._call(cmd_arg, attrs) + + attrs['view_config_changes.side_effect'] = [KeyboardInterrupt] + self.assertRaises( + KeyboardInterrupt, self._call, ['--debug'] + cmd_arg, attrs) + self._call(cmd_arg, attrs) + + attrs['view_config_changes.side_effect'] = [ValueError] + self.assertRaises( + ValueError, self._call, ['--debug'] + cmd_arg, attrs) + self._call(cmd_arg, attrs) if __name__ == '__main__': unittest.main() # pragma: no cover