Resolves Issue #342

This commit is contained in:
BJ Dierkes 2016-02-22 15:14:55 -06:00
parent 64ef916389
commit 53c4ef3862
7 changed files with 157 additions and 7 deletions

View File

@ -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

View File

@ -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)

93
cement/ext/ext_alarm.py Normal file
View File

@ -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 <https://docs.python.org/3.5/library/signal.html>`_.
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)

View File

@ -0,0 +1,9 @@
.. _cement.ext.ext_alarm:
:mod:`cement.ext.ext_alarm`
---------------------------
.. automodule:: cement.ext.ext_alarm
:members:
:private-members:
:show-inheritance:

View File

@ -47,6 +47,7 @@ Cement Extension Modules
.. toctree::
:maxdepth: 1
ext/ext_alarm
ext/ext_argcomplete
ext/ext_argparse
ext/ext_colorlog

View File

@ -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)

35
tests/ext/alarm_tests.py Normal file
View File

@ -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()