Signal Handling

Introduction to 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 (i.e. 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 collide.

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 so that the pre_close and post_close framework hooks get run for cleanup.

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() (or allow the with operator to exit properly).

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, SIGINT, and SIGHUP. When these signals are caught, Cement raises the exception CaughtSignal(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:

Example: Default Signal Handling
Example: Default Signal Handling
import signal
from cement import App, CaughtSignal
with App('myapp') as app:
try:
app.run()
except CaughtSignal as e:
# do something with e.signum or e.frame
if e.signum == signal.SIGTERM:
app.log.warning('Caught SIGTERM')
elif e.signum == signal.SIGINT:
app.log.warning('Caught SIGINT')

The above provides a very simple means of handling the most common signals, which in turn allows our application to "exit clean" by running app.close() and any pre_close or post_close hooks (via __exit__ from the with operator).

If we don't catch the signals, then the exceptions will be unhandled and the application will not exit clean.

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.

Example: Using the Signal Hook
Example: Using the Signal Hook
myapp.py
import signal
from cement import App, CaughtSignal
def my_signal_handler(app, signum, frame):
# do something with app
app.log.warning('Inside my_signal_handler')
# do something with signum, or frame
sig_name = signal.Signals(signum)
print('Caught Signal %s' % sig_name)
class MyApp(App):
class Meta:
label = 'myapp'
hooks = [
('signal', my_signal_handler),
]
with MyApp() as app:
try:
app.run()
except CaughtSignal as e:
# do something with e.signum, e.frame
pass

Alternatively for extensions and plugins:

def my_signal_handler(app, signum, frame):
# do something with app
app.log.warning('Inside my_signal_handler')
# do something with signum, or frame
sig_name = signal.Signals(signum)
print('Caught Signal %s' % sig_name)
def load(app):
app.hook.register('signal', my_signal_handler)

The key thing to note here is that the main application itself can easily handle the CaughtSignal exception without using hooks. However, using the 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.

Regardless of how signals are handled, all extensions or plugins should use the pre_close hook for cleanup purposes as much as possible as it is always run when app.close() is called.

Configuring Which Signals To Catch

You can define what signals to catch via App.Meta.catch_signals.

Example: Configuring Which Signals to Catch
Example: Configuring Which Signals to Catch
import signal
from cement import App
class MyApp(App):
class Meta:
label = 'myapp'
catch_signals = [
signal.SIGTERM,
signal.SIGINT,
signal.SIGHUP,
]

What happens is, Cement iterates over the App.Meta.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 at least run the cement signal hook within your callback.

The following is an example taken from the builtin cement_signal_handler callback. Note that there is a bit of hackery in how we are acquiring the CementApp from the frame. This is because the signal is picked up outside of our control so we need to find it.

Example: Overriding the Builtin Signal Handler Callback
Example: Overriding the Builtin Signal Handler Callback
from cement import App
def my_signal_handler(signum, frame):
"""
Catch a signal, run the ``signal`` hook, and then raise an exception
allowing the app to handle logic elsewhere.
Args:
signum (int): The signal number
frame: The signal frame
Raises:
cement.core.exc.CaughtSignal: Raised, passing ``signum``, and ``frame``
"""
LOG.debug('Caught signal %s' % signum)
for f_global in frame.f_globals.values():
if isinstance(f_global, App):
app = f_global
for res in app.hook.run('signal', app, signum, frame):
pass # pragma: nocover
raise exc.CaughtSignal(signum, frame)
class MyApp(App):
class Meta:
label = 'myapp'
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 App.Meta.catch_signals = None.