Search code examples
pythonobjectresource-cleanup

Ensuring instantiated objects do a controlled end if calling script dies


I have a script that instantiates a number of objects from other classes. When the calling script ends, these objects need to do some cleanup work, especially the closing and deleting of some tempfiles.

I've experimented with a number of ways to trigger the cleanup, and a number of them work when the calling script ends nicely...but none of them work in the event the calling script crashes or ends unexpectedly (like due to an uncaught error).

Is it possible to get the "child objects" (if that's the right term) to run their cleanup in the case of a crash or unexpected error?

Here's some test scripts...

THE OBJECT CLASS WHICH GETS CALLED...

import signal
import atexit
import os

class testme(object):
    def __init__(self):
        self.var = 1
        self.sigs()
        atexit.register(self.close)


    def __enter__(self):
        print "Hit enter"

    def sigs(self):
        for i in [x for x in dir(signal) if x.startswith("SIG")]:
          try:
            print "setting signal ", str(i)
            signum = getattr(signal,i)
            signal.signal(signum,self.close)
          except Exception, e:
            print ("Skipping " + str(i) + " in set_signal_handler")

    def __del__(self):
        print "Hit del"
        self.close()

    def __exit__(self, exception_type, exception_value, traceback):
        print "Hit exit"
        self.close()

    def close(self):
        print "Closing"
        # Perform cleanup actions here
        # Especially closing and deleting temp files

(The different signals that the "sigs()" function is [hopefully] setting is at the end of this post)

test2.py

import time
import testme
import sys

if __name__ == "__main__":
    with testme.testme() as obj:
        time.sleep(30)
        sys.exit()

test3.py

import time
import testme
import sys

if __name__ == "__main__":
    obj = testme.testme()
    time.sleep(30)
    sys.exit()

If I run test2.py and let it finish normally I get the following...

setting signal  SIGABRT
setting signal  SIGALRM
[...snip...]
Skipping SIG_DFL in set_signal_handler
setting signal  SIG_IGN
Hit enter
Hit exit
Closing
Closing
Hit del
Closing

If I run test2.py and use "kill -9" to end it (crash) I get...

setting signal  SIGABRT
[...snip...]
setting signal  SIG_IGN
Hit enter

If I run test3.py and let it finish, I get...

setting signal  SIGABRT
[...snip...]
setting signal  SIG_IGN
Closing
Hit del
Closing

If I run test3.py and use "kill -9" on it, I get the following...

setting signal  SIGABRT
[...snip...]
setting signal  SIG_IGN

(in other words...nothing)

EDIT: I did find that uncaught exceptions in the calling script do allow the del() in the child objects to run. Still wondering if there's a way to cleanly close out the child objects on other failures.

SIGNALS THAT (I hope) ARE GETTING SET BY sigs()...

setting signal  SIGABRT
setting signal  SIGALRM
setting signal  SIGBUS
setting signal  SIGCHLD
setting signal  SIGCLD
setting signal  SIGCONT
setting signal  SIGFPE
setting signal  SIGHUP
setting signal  SIGILL
setting signal  SIGINT
setting signal  SIGIO
setting signal  SIGIOT
setting signal  SIGKILL
Skipping SIGKILL in set_signal_handler
setting signal  SIGPIPE
setting signal  SIGPOLL
setting signal  SIGPROF
setting signal  SIGPWR
setting signal  SIGQUIT
setting signal  SIGRTMAX
setting signal  SIGRTMIN
setting signal  SIGSEGV
setting signal  SIGSTOP
Skipping SIGSTOP in set_signal_handler
setting signal  SIGSYS
setting signal  SIGTERM
setting signal  SIGTRAP
setting signal  SIGTSTP
setting signal  SIGTTIN
setting signal  SIGTTOU
setting signal  SIGURG
setting signal  SIGUSR1
setting signal  SIGUSR2
setting signal  SIGVTALRM
setting signal  SIGWINCH
setting signal  SIGXCPU
setting signal  SIGXFSZ
setting signal  SIG_DFL
Skipping SIG_DFL in set_signal_handler
setting signal  SIG_IGN    

Solution

  • Well your issue is that kill -9, aka SIGKILL, by its design cannot be caught. It is a guaranteed violent death for the offending process.

    The command kill sends the specified signal to the specified process or process group. If no signal is specified, the TERM signal is sent. The TERM signal will kill processes which do not catch this signal. For other processes, it may be necessary to use the KILL (9) signal, since this signal cannot be caught.

    For atexit to work properly, it needs to be able to catch some signal. You can instead try sending kill -15 (SIGTERM).