From 53817efffbd2ca0bd304b480ae08e4ad09d687e2 Mon Sep 17 00:00:00 2001 From: BJ Dierkes Date: Tue, 20 Dec 2011 18:34:24 -0600 Subject: [PATCH] further work on Issue #77 --- ChangeLog | 4 +- doc/source/api/core.rst | 4 +- doc/source/dev.rst | 1 + doc/source/dev/hooks.rst | 32 ++++- doc/source/dev/signal_handling.rst | 184 +++++++++++++++++++++++++ src/cement2/cement2/core/foundation.py | 26 +++- src/cement2/cement2/core/log.py | 5 +- 7 files changed, 242 insertions(+), 14 deletions(-) create mode 100644 doc/source/dev/signal_handling.rst diff --git a/ChangeLog b/ChangeLog index 652058f2..f25a6b8b 100755 --- a/ChangeLog +++ b/ChangeLog @@ -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: diff --git a/doc/source/api/core.rst b/doc/source/api/core.rst index 38d82dad..6ea0f0b2 100644 --- a/doc/source/api/core.rst +++ b/doc/source/api/core.rst @@ -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` diff --git a/doc/source/dev.rst b/doc/source/dev.rst index 7cc69713..2c0c6b1c 100644 --- a/doc/source/dev.rst +++ b/doc/source/dev.rst @@ -18,4 +18,5 @@ them in your applications. dev/controllers dev/hooks dev/plugins + dev/signal_handling dev/cleanup diff --git a/doc/source/dev/hooks.rst b/doc/source/dev/hooks.rst index 8839ccdc..b5e2ece7 100644 --- a/doc/source/dev/hooks.rst +++ b/doc/source/dev/hooks.rst @@ -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 + \ No newline at end of file diff --git a/doc/source/dev/signal_handling.rst b/doc/source/dev/signal_handling.rst new file mode 100644 index 00000000..6049ad22 --- /dev/null +++ b/doc/source/dev/signal_handling.rst @@ -0,0 +1,184 @@ +Signal Handling +=============== + +Python provides the `Signal `_ +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 `_ 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) \ No newline at end of file diff --git a/src/cement2/cement2/core/foundation.py b/src/cement2/cement2/core/foundation.py index 262dee66..0c77880c 100644 --- a/src/cement2/cement2/core/foundation.py +++ b/src/cement2/cement2/core/foundation.py @@ -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) diff --git a/src/cement2/cement2/core/log.py b/src/cement2/cement2/core/log.py index 161d7984..026f792e 100644 --- a/src/cement2/cement2/core/log.py +++ b/src/cement2/cement2/core/log.py @@ -1,4 +1,7 @@ -"""Cement core log module.""" +""" +Cement core log module. + +""" from cement2.core import exc, backend, interface