Version 16 of AtExit handlers

Updated 2003-07-19 20:13:20

AtExit handlers.

A common problem I had was that during an application I was setting up things like pipes (in the filesystem), add tons of temporary data, etc. I was then faced with the junk the program left after ending, so I was thinking of how to solve this, and was reminded of the atexit - C function.

So I wrote the following (it's quite short, that's why I include it here)

 # put this in a file and write the pkgIndex "magic"
 # package require AtExit
 # oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
 # When you call exit, calls any proc named after the scheme
 # AtExit_*. It may have arbitrary arguments as long as they
 # are default arguments (or no args at all of course). They are
 # called in no special order, except if the first argument's default
 # value is an integer: It is taken to be a priority then. The higher,
 # the earlier the proc is called
 # ==> Should the prio arg be forced to be named 'prio*' instead ? -Martin

 #rename exit to something else, install an exit
 # fcn which is capable of calling some cleanup
 # routines for, well, cleaning up.
 rename exit __shock
 namespace eval ::AtExit {
    variable outchann
    namespace export outchan print
    proc outchan { {chan stderr} } {
            variable outchann
            if {[info exists outchann]&&
                ($outchann != "stderr" && $outchann != "stdout")} {
                    close $outchann
            }
            set outchann $chan
    }
    proc noOutput { } {
            variable outchann
            if {[info exists outchann]&&
                ($outchann != "stderr" && $outchann != "stdout")} {
                    flush $outchann
                    close $outchann
            }
            catch { unset outchann }
    }
    # if you wonder what this one is for - I like, in the development
    # stage of a program, to print stuff of the exit handlers into the
    # exit-logfile (I just set ::debugging 1) - so I use
    # ::AtExit::print "text" in the cleanup routines for dbg msgs
    proc print { string } {
            variable outchann
            if {[info exists outchann]} {
                    puts $outchann $string
            }
    }
 }
 proc exit {{status 0}} {
    if {[info exists ::debugging] && $::debugging} {
        ::AtExit::outchan [open exit.log {WRONLY CREAT TRUNC}]
    }
    foreach proc [info procs AtExit_*] {
        if ![llength [info args $proc]] {
            lappend procs1 $proc
        } else {
            set bad 0
            foreach arg [set args [info args $proc]] {
                if ![info default $proc $arg junk] { set bad 1; break }
            }
            if {!$bad} {
                info default $proc [lindex $args 0] prio
                if [string is integer $prio] {
                    lappend procs_prio [list $prio $proc]
                }
            } else {
                ::AtExit::print "Cannot call finalizer <$proc> - it needs arguments!"
            }
        }
    }
    foreach proc [lsort -integer -decreasing -index 0 $procs_prio] {
        ::AtExit::print "AtExit is calling finalizer <$proc>"
        if [catch {uplevel #0 $proc}] {
            ::AtExit::print "...Failure in handler!"
        }
    }
    foreach proc $procs1 {
        ::AtExit::print "AtExit is calling finalizer <prio: [lindex $proc 0],proc [set proc [lindex $proc 1]]>" 
        if [catch {uplevel #0 $proc}] {
            ::AtExit::print "...Failure in handler!"
        }
    }
    # This is no AtExit handler because it needs to
    # be called as absolutely last thing.
    if {[info exists ::AtExit::outchann] && 
        ($::AtExit::outchann != "stderr" && $::AtExit::outchann != "stdout")} {
            puts $::AtExit::outchann "Done handling exit handlers, closing stream and exitting."
            close $::AtExit::outchann
    }
    # bye bye
    __shock $status
 }
 package provide AtExit 0.1

This way you write some AtExit_* functions which expect no arguments, and which are called once you call exit. If you want to exit without calling those handlers, you can always shock (__shock) your program.

Another thing which comes handy, is to install a handler for your window if you use tk:

 wm protocol . WM_DELETE_WINDOW exit

This way even if you kill your application with the help of the windowmanager, exit will be called, and thus your exit-handlers will be called, too.

I suppose this can be easily enhanced so the exit handlers have a priority; one possibility which comes to my mind is declaring the procs so that they have a default argument 'priority'; the list of procs to call would then be sorted by the values of those default parameters.. Oh, yes, it's quite easy. Suppose once I'm home and have time again I'll do just that :)

Okay, did just that. Hope you enjoy it. -MSW


MSW: Catching the uplevel'd procs now - if you're only prepared (e.g. GUI wise) to call exit rather than __shock, you might end up with a faulty AtExit_* handler which prevents you from exitting and forces you to kill the application the hard way - which would mean your handler don't get called again...


Martin Lemburg - Sep. 10th, 2002: Inspired by the discussion about exit handlers, I wrote a package AtExit, that works in a quite different way. My AtExit package takes callbacks and priorities. The execution rule is:

  1. The higher the priority, the earlier the execution
  2. The later the callback is added, the earlier the execution

Every callback is executed by uplevel in the scope of the procedure calling exit. Arguments are allowed, but must be satisfied by the callback definition (e.g. list tk_messageBox -message "Dummy") or by substitutions:

  • %t token of the exit handler
  • %p priority of the exit handler
  • %l level of the caller of exit
  • %c caller of exit
  • %a args of the caller exit

An exit handler can be registered as event handler for the WM_DELETE_WINDOW event of a widget (than it is disabled as common exit handler). The former event handler is stored and will be restored, if the exit handler is unregistered again.

Download via:

     ftp://ftp.dcade.de/pub/ml/tcl/packages/AtExit.tar.gz
     ftp://ftp.dcade.de/pub/ml/tcl/packages/AtExit.zip

MS contributes a very small and simple-minded atExit handler:

 namespace eval AtExit {
    variable atExitScrips [list]

    proc atExit script {
        variable atExitCmds
        lappend atExitCmds \
                [uplevel 1 {namespace code $script}]
    }

    namespace export atExit
 }

 rename exit AtExit::ExitOrig
 proc exit {{code 0}} {
     variable AtExit::atExitScripts
     set n [llength $atExitScripts]
     while {$n} {
        catch [lindex $atExitScripts [incr n -1]]
     }
     rename AtExit::ExitOrig exit
     namespace delete AtExit
     exit $code
 }

 namespace import AtExit::atExit

The usage is really quite simple, similar to other callbacks. For example

 set f [open MyFile]
 atExit [list close $f]

insures that the file will be closed at program exit.

Remark that the atExit scripts run in the namespace where atExit was called. Hence, they have access to the namespace's commands and variables.