Exit gracefully on Ctrl-C

If we receive SIGINT, exit gracefully: run the clean_up method; don't
print a stacktrace; exit with error code 130 (128 + SIGINT).

Change-Id: I77687133d5482912523814a28e42f4f3a1a146d5
Story: 2008124
Task: 40846
This commit is contained in:
Zane Bitter 2020-08-25 16:31:05 -04:00
parent 7fdd7cb4c5
commit fe7475d6c5
2 changed files with 59 additions and 16 deletions

View File

@ -276,11 +276,16 @@ class App(object):
else:
self.LOG.error(err)
return 1
except KeyboardInterrupt:
return 130
result = 1
if self.interactive_mode:
result = self.interact()
else:
result = self.run_subcommand(remainder)
try:
result = self.run_subcommand(remainder)
except KeyboardInterrupt:
return 130
return result
# FIXME(dhellmann): Consider moving these command handling methods
@ -391,6 +396,7 @@ class App(object):
kwargs['cmd_name'] = cmd_name
cmd = cmd_factory(self, self.options, **kwargs)
result = 1
err = None
try:
self.prepare_to_run_command(cmd)
full_name = (cmd_name
@ -398,17 +404,24 @@ class App(object):
else ' '.join([self.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
try:
parsed_args = cmd_parser.parse_args(sub_argv)
except SystemExit as ex:
raise cmd2.exceptions.Cmd2ArgparseError from ex
result = cmd.run(parsed_args)
except help.HelpExit:
result = 0
except SystemExit as ex:
raise cmd2.exceptions.Cmd2ArgparseError from ex
except Exception as err:
except Exception as err1:
err = err1
if self.options.debug:
self.LOG.exception(err)
else:
self.LOG.error(err)
except KeyboardInterrupt as err1:
result = 130
err = err1
raise
finally:
try:
self.clean_up(cmd, result, err)
except Exception as err2:
@ -416,15 +429,5 @@ class App(object):
self.LOG.exception(err2)
else:
self.LOG.error('Could not clean up: %s', err2)
if self.options.debug:
# 'raise' here gets caught and does not exit like we want
return result
else:
try:
self.clean_up(cmd, result, None)
except Exception as err3:
if self.options.debug:
self.LOG.exception(err3)
else:
self.LOG.error('Could not clean up: %s', err3)
del err
return result

View File

@ -51,6 +51,15 @@ def make_app(**kwargs):
err_command.return_value = err_command_inst
cmd_mgr.add_command('error', err_command)
# Register a command that is interrrupted
interrupt_command = mock.Mock(name='interrupt_command', spec=c_cmd.Command)
interrupt_command_inst = mock.Mock(spec=c_cmd.Command)
interrupt_command_inst.run = mock.Mock(
side_effect=KeyboardInterrupt
)
interrupt_command.return_value = interrupt_command_inst
cmd_mgr.add_command('interrupt', interrupt_command)
app = application.App('testing interactive mode',
'1',
cmd_mgr,
@ -113,6 +122,11 @@ class TestInitAndCleanup(base.TestBase):
app.run(['mock'])
app.prepare_to_run_command.assert_called_once_with(command())
def test_interrupt_command(self):
app, command = make_app()
result = app.run(['interrupt'])
self.assertEqual(result, 130)
def test_clean_up_success(self):
app, command = make_app()
app.clean_up = mock.MagicMock(name='clean_up')
@ -148,6 +162,19 @@ class TestInitAndCleanup(base.TestBase):
self.assertIsInstance(args[2], RuntimeError)
self.assertEqual(('test exception',), args[2].args)
def test_clean_up_interrupt(self):
app, command = make_app()
app.clean_up = mock.MagicMock(name='clean_up')
ret = app.run(['interrupt'])
self.assertNotEqual(ret, 0)
app.clean_up.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY)
call_args = app.clean_up.call_args_list[0]
self.assertEqual(mock.call(mock.ANY, 130, mock.ANY), call_args)
args, kwargs = call_args
self.assertIsInstance(args[2], KeyboardInterrupt)
def test_error_handling_clean_up_raises_exception(self):
app, command = make_app()
@ -322,6 +349,19 @@ class TestHelpHandling(base.TestBase):
def test_deferred_help(self):
self._test_help(True)
def _test_interrupted_help(self, deferred_help):
app, _ = make_app(deferred_help=deferred_help)
with mock.patch('cliff.help.HelpAction.__call__',
side_effect=KeyboardInterrupt):
result = app.run(['--help'])
self.assertEqual(result, 130)
def test_interrupted_help(self):
self._test_interrupted_help(False)
def test_interrupted_deferred_help(self):
self._test_interrupted_help(True)
def test_subcommand_help(self):
app, _ = make_app(deferred_help=False)