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:
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:
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.
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?.