diff --git a/ChangeLog b/ChangeLog index 68296d3f..e2334457 100755 --- a/ChangeLog +++ b/ChangeLog @@ -47,7 +47,8 @@ Features: * :issue:`322` - Added Tabulate Framework Extension (tabulatized output) * :issue:`336` - Added Support for ``CementApp.reload()`` (SIGHUP) * :issue:`337` - Added ``app.run_forever()`` alternative run method. - + * :issue:`342` - Added Alarm/Timeout Support + Refactoring: * :issue:`311` - Refactor Hooks/Handlers into CementApp diff --git a/cement/core/foundation.py b/cement/core/foundation.py index c8f8cc45..4b22d124 100644 --- a/cement/core/foundation.py +++ b/cement/core/foundation.py @@ -757,8 +757,8 @@ class CementApp(meta.MetaMixin): for res in self.hook.run('pre_setup', self): pass - self._setup_signals() self._setup_extension_handler() + self._setup_signals() self._setup_config_handler() self._setup_mail_handler() self._setup_cache_handler() @@ -1073,16 +1073,26 @@ class CementApp(meta.MetaMixin): for res in self.hook.run('post_argument_parsing', self): pass + def catch_signal(self, signum): + """ + Add ``signum`` to the list of signals to catch and handle by Cement. + + :param signum: The signal number to catch. See Python ``signal`` + library. + """ + + LOG.debug("adding signal handler %s for signal %s" % ( + self._meta.signal_handler, signum) + ) + signal.signal(signum, self._meta.signal_handler) + def _setup_signals(self): if self._meta.catch_signals is None: LOG.debug("catch_signals=None... not handling any signals") return for signum in self._meta.catch_signals: - LOG.debug("adding signal handler %s for signal %s" % ( - self._meta.signal_handler, signum) - ) - signal.signal(signum, self._meta.signal_handler) + self.catch_signal(signum) def _resolve_handler(self, handler_type, handler_def, raise_error=True): han = self.handler.resolve(handler_type, handler_def, raise_error) diff --git a/cement/ext/ext_alarm.py b/cement/ext/ext_alarm.py new file mode 100644 index 00000000..a8fc2e6c --- /dev/null +++ b/cement/ext/ext_alarm.py @@ -0,0 +1,93 @@ +""" +The Alarm Extension provides easy access to setting an application alarm to +handle timing out operations. See the +`Python Signal Library `_. + +Availability: Unix + +Example: + +.. code-block:: python + + import time + from cement.core.foundation import CementApp + from cement.core.exc import CaughtSignal + + + class MyApp(CementApp): + class Meta: + label = 'myapp' + exit_on_close = True + extensions = ['alarm'] + + + with MyApp() as app: + try: + app.run() + app.alarm.set(3, "The operation timed out after 3 seconds!") + + # do something that takes time to operate + time.sleep(5) + + app.alarm.stop() + + except CaughtSignal as e: + print(e.msg) + app.exit_code = 1 + +Looks like: + +.. code-block:: console + + $ python myapp.py + ERROR: The operation timed out after 3 seconds! + Caught signal 14 + +""" + +import signal +from ..utils.misc import minimal_logger + +LOG = minimal_logger(__name__) + + +def alarm_handler(app, signum, frame): + if signum == signal.SIGALRM: + app.log.error(app.alarm.msg) + +class AlarmManager(object): + """ + Lets the developer easily set and stop an alarm. If the + alarm exceeds the given time it will raise ``signal.SIGALRM``. + + """ + def __init__(self, *args, **kw): + super(AlarmManager, self).__init__(*args, **kw) + self.msg = None + + def set(self, time, msg): + """ + Set the application alarm to ``time`` seconds. If the time is + exceeded ``signal.SIGALRM`` is raised. + + :param time: The time in seconds to set the alarm to. + :param msg: The message to display if the alarm is triggered. + """ + + LOG.debug('setting application alarm for %s seconds' % time) + self.msg = msg + signal.alarm(int(time)) + + def stop(self): + """ + Stop the application alarm. + """ + LOG.debug('stopping application alarm') + signal.alarm(0) + + +def load(app): + app.catch_signal(signal.SIGALRM) + app.extend('alarm', AlarmManager()) + app.hook.register('signal', alarm_handler) + diff --git a/doc/source/api/ext/ext_alarm.rst b/doc/source/api/ext/ext_alarm.rst new file mode 100644 index 00000000..e3f51ade --- /dev/null +++ b/doc/source/api/ext/ext_alarm.rst @@ -0,0 +1,9 @@ +.. _cement.ext.ext_alarm: + +:mod:`cement.ext.ext_alarm` +--------------------------- + +.. automodule:: cement.ext.ext_alarm + :members: + :private-members: + :show-inheritance: diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst index d0539c8c..3ae252fe 100644 --- a/doc/source/api/index.rst +++ b/doc/source/api/index.rst @@ -47,6 +47,7 @@ Cement Extension Modules .. toctree:: :maxdepth: 1 + ext/ext_alarm ext/ext_argcomplete ext/ext_argparse ext/ext_colorlog diff --git a/tests/core/foundation_tests.py b/tests/core/foundation_tests.py index 1c79c85d..e5b57fbb 100644 --- a/tests/core/foundation_tests.py +++ b/tests/core/foundation_tests.py @@ -11,6 +11,7 @@ from cement.core.controller import CementBaseController, expose from cement.core import log, output, hook, arg, controller from cement.core.interface import Interface from cement.utils import test +from cement.core.exc import CaughtSignal from cement.utils.misc import init_defaults, rando, minimal_logger from nose.plugins.attrib import attr @@ -491,4 +492,4 @@ class FoundationTestCase(test.CementCoreTestCase): self.eq(e.args[0], 'It ran forever!') raise finally: - signal.alarm(0) + signal.alarm(0) diff --git a/tests/ext/alarm_tests.py b/tests/ext/alarm_tests.py new file mode 100644 index 00000000..4a2284de --- /dev/null +++ b/tests/ext/alarm_tests.py @@ -0,0 +1,35 @@ +"""Tests for cement.ext.ext_alarm.""" + +import sys +import time +import signal +from cement.core.exc import CaughtSignal +from cement.utils import test + + +class AlarmExtTestCase(test.CementExtTestCase): + + def setUp(self): + self.app = self.make_app('tests', + extensions=['alarm'], + argv=[] + ) + + @test.raises(CaughtSignal) + def test_alarm_timeout(self): + global app + app = self.app + with app as app: + try: + app.alarm.set(1, "The Timer Works!") + time.sleep(3) + except CaughtSignal as e: + self.eq(e.signum, signal.SIGALRM) + raise + + def test_alarm_no_timeout(self): + with self.app as app: + app.alarm.set(3, "The Timer Works!") + time.sleep(1) + app.alarm.stop() + \ No newline at end of file