Search code examples
pythonoopsignals

More than one SIGTERM handler in a Python script - who should call sys.exit()?


Let's say I have the script below:

import signal
import sys

class Klass:
   def __init__(self, name):
     self.name = name

     def disconnect_gracefully(*args):
         print(self.name)
         sys.exit()

     signal.signal(signal.SIGINT, disconnect_gracefully)


a = Klass("A")
b = Klass("B")

while True:
    pass

Note that both classes have a graceful exit after a SIGINT. When I run this, and crtl-c, only "B" is printed.

Generally, in a case like this, who or what should call sys.exit() - should both instances, neither?


Solution

  • The Python documentation for signal only has this to say about what happens when you set a handler for the same signal multiple times:

    A handler for a particular signal, once set, remains installed until it is explicitly reset

    and

    [when calling signal.signal] The previous signal handler will be returned

    which seems to imply that, like POSIX signals, a process can have just a single handler for a given signal at a time.

    We can add some statements to your program to show what signal.signal returns (in CPython):

    import signal
    import sys
    class K:
      def __init__(self, name):
        self.name=name
        def disconnect(*args):
          print(self.name)
          sys.exit()
        self.disconnectfuncid=id(disconnect)
        self.oldsig=signal.signal(signal.SIGINT, disconnect)
    
    
    >>> a=K("a")
    >>> hex(a.disconnectfuncid)
    '0x7fc5c1057f28'
    >>> a.oldsig
    <built-in function default_int_handler>
    >>> signal.getsignal(signal.SIGINT)
    <function K.__init__.<locals>.disconnect at 0x7fc5c1057f28>
    
    
    >>> b=K("b")
    >>> hex(b.disconnectfuncid)
    '0x7fc5c0ed0950'
    >>> b.oldsig
    <function K.__init__.<locals>.disconnect at 0x7fc5c1057f28>
    >>> signal.getsignal(signal.SIGINT)
    <function K.__init__.<locals>.disconnect at 0x7fc5c0ed0950>
    
    
    >>> while True:
    ...   pass
    ...^C b
    

    So the handler starts out as <built-in function default_int_handler>, then gets set to the disconnect function in the a instance, then gets set to the disconnect function in the b instance, and the latter is what gets called when the signal is delivered.

    If you want to exit when receiving SIGINT, and run multiple functions just before exit, one way to do that is to call the following (which will suppress printing the traceback and KeyboardInterrupt messages):

     signal.signal(signal.SIGINT, lambda *args: sys.exit())
    

    and then register each function using atexit.register.