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.
By default Cement catches the signals
SIGHUP. When these signals are caught, Cement raises the exception
CaughtSignal(signum, frame) where
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 signalfrom cement import App, CaughtSignalwith App('myapp') as app:try:app.run()except CaughtSignal as e:# do something with e.signum or e.frameif 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
post_close hooks (via
__exit__ from the
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.
myapp.pyimport signalfrom cement import App, CaughtSignaldef my_signal_handler(app, signum, frame):# do something with appapp.log.warning('Inside my_signal_handler')# do something with signum, or framesig_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.framepass
Alternatively for extensions and plugins:
def my_signal_handler(app, signum, frame):# do something with appapp.log.warning('Inside my_signal_handler')# do something with signum, or framesig_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.
You can define what signals to catch via App.Meta.catch_signals.
import signalfrom cement import Appclass 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
frame parameters, you are able to handle the logic elsewhere rather than assigning a unique callback function for every signal.
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.
from cement import Appdef my_signal_handler(signum, frame):"""Catch a signal, run the ``signal`` hook, and then raise an exceptionallowing the app to handle logic elsewhere.Args:signum (int): The signal numberframe: The signal frameRaises: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_globalfor res in app.hook.run('signal', app, signum, frame):pass # pragma: nocoverraise exc.CaughtSignal(signum, frame)class MyApp(App):class Meta:label = 'myapp'signal_handler = my_signal_handler
To each their own. If you simply do not want any kind of signal handling performed, just set
App.Meta.catch_signals = None.