Updated 2012-01-21 06:56:53 by RLE

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 atExitScripts [list]
 
    proc atExit script {
        variable atExitScripts
        lappend atExitScripts \
                [uplevel 1 [list 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 exit {}
     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 $MyTempFile ]
 atExit [list close $f]
 atExit [list file delete -force $MyTempFile]

insures that the temporary file will be closed and deleted 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.

lv there are at least 3 ways one can find oneself in an exitted state.

  1. invocation of exit
  2. catestrophic event - divide by zero, some other interpreter crash
  3. external intervention (aka the hand of God syndrome) - such as a termination signal, processor rebooted, etc.

The non-expert Tcl developer should note that AtExit takes care of the first of these. Judicial use of catch can attempt to take care of some of the second of these - but not all. The default Tcl has nothing to deal with the hand of God syndrome. One can handle most of these events via tclx if its trap capability is available on your platform. Otherwise, one has to write some custom code.

EKB 19 March 2005

When exiting in Windows, it's nice to store preferences either in the Registry or in the user's "Application Data" folder. Because I basically do Unix-style programming in a Windows environment, I prefer the second option (the closest thing to a .foo file).

GWM the Application Data directory is best found from env(APPDATA) on Win2000, XP, Vista, Win7 since at least the last 2 OS's do not use the directory named "Application Data" but "AppData/Roaming" or similar.

Here's a snippet with two routines to help do that (edited by EKB 16 April 2005 to fix a couple of bugs):
 proc LoadPrefs {progname prefsfile} {
   global USERDIR USERPREFS
   
   # Get current user's home directory: If environment vars not available,
   # default to subfolder of the installation folder
   set USERDIR [file dirname $argv0]
   if {$tcl_platform(os) == "Windows NT"} {
     if {[info exists env(USERPROFILE)]} {set USERDIR $env(USERPROFILE)}
   }
   if {$tcl_platform(os) == "Windows 95"} {
     if {[info exists env(windir)] && [info exists env(USERNAME)]} {
       set USERDIR [file join $env(windir) Profiles $env(USERNAME)]
   }
   set USERDIR [file join $USERDIR "Application Data" $progname]
   set USERPREFS [file join $USERDIR $prefsfile]

   if {[file exists $USERPREFS]} {
     source $USERPREFS
   }
 }

 proc SavePrefs {} {
    global prefs USERDIR USERPREFS

    if {![file exists $USERDIR]} {file mkdir $USERDIR}
      
    # Find out if the window is zoomed
    if {[wm state .] == "zoomed"} {
      # Set isMaximized, but not geometry
      # (This way, when unzoom, will go back to a sensible size)
      set prefs(isMaximized) true
    } else {
      # Store the current window geometry if not zoomed
      set prefs(geometry) [wm geometry .]
      set prefs(isMaximized) false
    }

    # Don't bother about errors. If can't open, then can't save prefs. That's OK.
    if {![catch {open $USERPREFS w} fileID]} {
      foreach item [array names prefs] {
        puts $fileID "set prefs($item) \"$prefs($item)\""
      }
      close $fileID
    }
 }

They can be used this way:

 # Set defaults for preferences
 set prefs(geometry) 300x200
 set prefs(isMaximized) false
 set prefs(...) ... # Set your own
 ...
 
 # Load preferences
 LoadPrefs "My Program" prefs.tcl
 
 # Apply preferences
 wm geometry . $prefs(geometry)
 if {$prefs(isMaximized)} {
   wm state . zoomed
 }
 ... # Apply your own
 
 # At exit, save prefs
 SavePrefs

MG 19 March 2005 - You can also get AtExit handles using the trace command. Instead of redefining exit, just use
  trace add execution exit enter YourCleanupProc

and YourCleanupProc will be run before the exit command is. (You'll still need to include
 wm protocol . WM_DELETE_WINDOW exit

if your code uses Tk, so clicking the X in the title bar, etc, will run exit and trigger the trace.)

MSW(2005-03-20) notes that this will only work with 8.4+

makr (2008-11-26): If you happen to use Expect anyway, you may want to have a look at exp_exit -onexit ?handler?.