further work on Issue #77

This commit is contained in:
BJ Dierkes 2011-12-20 18:34:24 -06:00
parent 15eec25240
commit 53817efffb
7 changed files with 242 additions and 14 deletions

View File

@ -34,7 +34,9 @@ Feature Enhancements:
* :issue:`76` - Add app.close() functionality including a
cement_on_close_hook() allowing plugins/extensions/etc to be able to
cleanup on application exit.
* :issue:`77` - Add signal handler for SIGINT/SIGTERM/Etc
* :issue:`77` - Added default signal handler for SIGINT/SIGTERM as well as
the cement_signal_hook which is called when any catch_signals are
encountered.
Incompatible Changes:

View File

@ -87,8 +87,7 @@ Cement2 Core Modules
-----------------------
.. automodule:: cement2.core.log
:members:
:members:
.. _cement2.core.output:
@ -98,7 +97,6 @@ Cement2 Core Modules
.. automodule:: cement2.core.output
:members:
.. _cement2.core.plugin:
:mod:`cement2.core.plugin`

View File

@ -18,4 +18,5 @@ them in your applications.
dev/controllers
dev/hooks
dev/plugins
dev/signal_handling
dev/cleanup

View File

@ -201,8 +201,8 @@ Cement has a number of hooks that tie into the framework.
cement_pre_setup_hook
^^^^^^^^^^^^^^^^^^^^^
Run before CementApp.setup() is called. The application object object is
passed as an argument.
Run before CementApp.setup() is called. The application object is
passed as an argument.
.. code-block:: python
@ -261,10 +261,36 @@ passed as an argument.
@hook.register(name='cement_post_run_hook')
def my_post_run_hook(app):
# Do something after application run() is called.
pass
return
cement_on_close_hook
^^^^^^^^^^^^^^^^^^^^
Run when app.close() is called. This hook should be used by plugins and
extensions to do any 'cleanup' at the end of program execution.
.. code-block:: python
from cement2.core import hook
@hook.register(name='cement_on_close_hook')
def my_cleanup_hook(app):
# Do something when the application close() is called.
return
cement_signal_hook
^^^^^^^^^^^^^^^^^^
Run when signal handling is enabled, and the defined signal handler callback
is executed. This hook should be used by the application, plugins, and
extensions to perform any actions when a specific signal is caught.
.. code-block:: python
from cement2.core import hook
@hook.register(name='cement_signal_hook')
def my_signal_hook(signum, frame):
# do something with signum/frame
return

View File

@ -0,0 +1,184 @@
Signal Handling
===============
Python provides the `Signal <http://docs.python.org/library/signal.html>`_
library allowing developers to catch Unix signals and set handlers for
asynchronous events. For example, the 'SIGTERM' (Terminate) signal is
received when issuing a 'kill' command for a given Unix process. Via the
signal library, we can set a handler (function) callback that will be executed
when that signal is received. Some signals however can not be handled/caught,
such as the SIGKILL signal (kill -9). Please refer to the
`Signal <http://docs.python.org/library/signal.html>`_ library documentation
for a full understanding of its use and capabilities.
A caveat when setting a signal handler is that only one handler can be defined
for a given signal. Therefore, all handling must be done from a single
callback function. This is a slight roadblock for applications built on
Cement in that many pieces of the framework are broken out into independent
extensions as well as applications that have 3rd party plugins. The trouble
happens when the application, plugins, and framework extensions all need to
perform some action when a signal is caught. This section outlines the
recommended way of handling signals with Cement versus manually setting signal
handlers that may.
*Important Note*
It is important to note that it is not necessary to use the Cement mechanisms
for signal handling, what-so-ever. That said, the primary concern of the
framework is that app.close() is called no matter what the situation.
Therefore, if you decide to disable signal handling all together you *must*
ensure that you at the very least catch signal.SIGTERM and signal.SIGINT with
the ability to call app.close(). You will likely find that it is more
complex than you might think. The reason we put these mechanisms in place is
primarily that we found it was the best way to a) handle a signal, and b) have
access to our 'app' object in order to be able to call 'app.close()' when a
process is terminated.
Signals Caught by Default
-------------------------
By default Cement catches the signals SIGTERM and SIGINT. When these signals
are caught, Cement raises the exception 'CementSignalError(signum, frame)'
where 'signum' and 'frame' are the parameters passed to the signal handler.
By raising an exception, we are able to pass runtime back to our applications
main process (within a try/except block) and maintain the ability to access
our 'application' object without using global objects.
A basic application using default handling might look like:
.. code-block:: python
import signal
from cement2.core import foundation, exc
app = foundation.lay_cement('myapp')
app.setup()
try:
app.run()
except exc.CementSignalError as e:
# do something with e.signum or e.frame (passed from signal library)
if e.signum == signal.SIGTERM:
print("Caught signal SIGTERM...")
# do something to handle signal here
elif e.signum == signal.SIGINT:
print("Caught signal SIGINT...")
# do something to handle signal here
finally:
app.close()
As you can see, this provides a very simple means of handling the most common
signals allowing us to still call app.close() after handling the signal. This
is extremely important as 'app.close()' is where the 'cement_on_close_hook' is
called, allowing the framework/application/extensions/plugins to all perform
any cleanup actions they may need.
Using The Signal Hook
---------------------
An alternative way of adding multiple callbacks to a signal handler is by
using the cement_signal_hook. This hook is called anytime a handled signal
is encountered.
.. code-block:: python
import signal
from cement2.core import foundation, exc, hook
app = foundation.lay_cement('myapp')
app.setup()
@hook.register(name='cement_signal_hook')
def my_signal_handler(signum, frame):
# do something with signum/frame
if signum == signal.SIGTERM:
print("Caught signal SIGTERM...")
# do something to handle signal here
elif signum == signal.SIGINT:
print("Caught signal SIGINT...")
# do something to handle signal here
try:
app.run()
except exc.CementSignalError as e:
pass
finally:
app.close()
The key thing to note here is that the main application itself handles the
exc.CementSignalError exception, where as using the cement_signal_hook is
useful for plugins and extensions to be able to tie into the signal handling
outside of the main application. Both serve the same purpose however the
application object is not available (passed to) the cement_signal_hook which
limits what can be done within the callback function. For this reason
any extensions or plugins should use the cement_on_close_hook as much as
possible as it is always run when app.close() is called and receives the
app object as one of its parameters.
Configuring Which Signals To Catch
----------------------------------
You can define other signals to catch by passing a list of 'catch_signals' to
foundation.lay_cement():
.. code-block:: python
import signal
from cement2.core import foundation, exc
SIGNALS = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]
app = foundation.lay_cement('myapp', catch_signals=SIGNALS)
...
What happens is, Cement iterates over the catch_signals list and adds a
generic handler function (the same) for each signal. Because the handler
calls the cement_signal_hook, and then raises an exception which both pass the
'signum' and 'frame' parameters, you are able to handle the logic elsewhere
rather than assigning a unique callback function for every signal.
What If I Don't Like Your Signal Handler Callback?
--------------------------------------------------
If you want more control over what happens when a signal is caught, you are
more than welcome to override the default signal handler callback. That said,
please be kind and be sure to atleast run the cement_signal_hook within your
callback.
.. code-block:: python
import signal
from cement2.core import foundation, exc, hook
SIGNALS = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]
def my_signal_handler(signum, frame):
# do something with signum/frame
print 'Caught signal %s' % signum
# execute the cement_signal_hook
for res in hook.run('cement_signal_hook', signum, frame):
pass
app = foundation.lay_cement('myapp',
catch_signals=SIGNALS,
signal_handler=my_signal_handler)
...
This Is Stupid, and UnPythonic - How Do I Disable It?
-----------------------------------------------------
To each their own. If you simply do not want any kind of signal handling
performed, just set 'catch_signals=None'.
.. code-block:: python
import signal
from cement2.core import foundation, exc
app = foundation.lay_cement('myapp', catch_signals=None)

View File

@ -20,8 +20,12 @@ def cement_signal_handler(signum, frame):
"""
Log.debug('Caught signal %s' % signum)
raise exc.CementSignalError(signum, frame)
for res in hook.run('cement_signal_hook', signum, frame):
pass
raise exc.CementSignalError(signum, frame)
class CementApp(object):
"""
The CementApp is the primary application class used and returned by
@ -44,6 +48,10 @@ class CementApp(object):
List of signals to catch, and raise exc.CementSignalError for.
Default: [signals.SIGTERM, signals.SIGINT]
signal_handler
Func to handle any caught signals.
Default: cement.core.foundation.cement_signal_handler
config_handler
An instantiated config handler object.
@ -73,7 +81,8 @@ class CementApp(object):
self.argv = kw.get('argv', sys.argv[1:])
self.catch_signals = kw.get('catch_signals',
[signal.SIGTERM, signal.SIGINT])
self.signal_handler = kw.get('signal_handler', cement_signal_handler)
# default all handlers to None
self.ext = None
self.config = None
@ -267,9 +276,13 @@ class CementApp(object):
self._setup_output_handler()
def _setup_signals(self):
if not self.catch_signals:
Log.debug("catch_signals=None... not handling any signals")
return
for signum in self.catch_signals:
Log.debug("adding signal handler for signal %s" % signum)
signal.signal(signum, cement_signal_handler)
signal.signal(signum, self.signal_handler)
def _setup_extension_handler(self):
Log.debug("setting up %s.extension handler" % self.name)
@ -382,15 +395,15 @@ class CementApp(object):
def lay_cement(name, klass=CementApp, *args, **kw):
"""
Initialize the framework. All *args, and **kwargs are passed to the
Initialize the framework. All args, and kwargs are passed to the
klass() object.
Required Arguments:
name
The name of the application.
Optional Keyword Arguments:
klass
@ -439,6 +452,7 @@ def lay_cement(name, klass=CementApp, *args, **kw):
hook.define('cement_pre_run_hook')
hook.define('cement_post_run_hook')
hook.define('cement_on_close_hook')
hook.define('cement_signal_hook')
# define and register handlers
handler.define(extension.IExtension)

View File

@ -1,4 +1,7 @@
"""Cement core log module."""
"""
Cement core log module.
"""
from cement2.core import exc, backend, interface