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 ‘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:

import signal
from cement.core.foundation import CementApp
from cement.core.exc import CaughtSignal

with CementApp('myapp') as app:
    try:
        app.run()
    except CaughtSignal as e:
        # do something with e.signum or e.frame (passed from signal)
        if e.signum == signal.SIGTERM:
            print("Caught SIGTERM...")
        elif e.signum == signal.SIGINT:
            print("Caught SIGINT...")

The above provides a very simple means of handling the most common signals, which in turns allowes our application to “exit clean” by running app.close() and any pre_close or post_close hooks. 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.

import signal
from cement.core.foundation import CementApp
from cement.core.exc import CaughtSignal

def my_signal_handler(app, signum, frame):
    # do something with app?
    pass

    # or do someting with signum or frame
    if signum == signal.SIGTERM:
        print("Caught SIGTERM...")
    elif signum == signal.SIGINT:
        print("Caught SIGINT...")

with CementApp('myapp') as app:
    hook.register('signal', my_signal_handler)

    try:
        app.run()
    except CaughtSignal as e:
        # do soemthing with e.signum, e.frame
        pass

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 other signals to catch by passing a list of ‘catch_signals’ to foundation.CementApp():

import signal
from cement.core.foundation import CementApp

SIGNALS = [signal.SIGTERM, signal.SIGINT, signal.SIGHUP]

CementApp('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.

The following is an example taken from the builtin callback handler. 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.

import signal
from cement.core.foundation import CementApp

def cement_signal_handler(signum, frame):
    """
    Catch a signal, run the ``signal`` hook, and then raise an exception
    allowing the app to handle logic elsewhere.

    :param signum: The signal number
    :param frame: The signal frame.
    :raises: cement.core.exc.CaughtSignal

    """
    LOG.debug('Caught signal %s' % signum)

    # hackish, but we do not have direct access to the CementApp object
    for f_global in frame.f_globals.values():
        if isinstance(f_global, CementApp):
            app = f_global
            for res in app.hook.run('signal', app, signum, frame):
                pass
    raise exc.CaughtSignal(signum, frame)


with CementApp('myapp') as app:
    try:
        app.run()
    except CaughtSignal as e:
        # do something with e.signum, or e.frame
        pass

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.

from cement.core.foundation import foundation

CementApp('myapp', catch_signals=None)