Monday, March 1, 2010

How to capture signals from bash

From: http://www.davidpashley.com/articles/writing-robust-shell-scripts.html

Setting traps

Often you write scripts which fail and leave the filesystem in an inconsistent state; things like lock files, temporary files or you've updated one file and there is an error updating the next file. It would be nice if you could fix these problems, either by deleting the lock files or by rolling back to a known good state when your script suffers a problem. Fortunately bash provides a way to run a command or function when it receives a unix signal using the trapcommand.

trap command signal [signal ...]

There are many signals you can trap (you can get a list of them by running kill -l), but for cleaning up after problems there are only 3 we are interested in: INT, TERM and EXIT. You can also reset traps back to their default by using - as the command.

SignalDescription
INTInterrupt - This signal is sent when someone kills the script by pressing ctrl-c.
TERMTerminate - this signal is sent when someone sends the TERM signal using the kill command.
EXITExit - this is a pseudo-signal and is triggered when your script exits, either through reaching the end of the script, an exit command or by a command failing when using set -e.

Usually, when you write something using a lock file you would use something like:

if [ ! -e $lockfile ]; then    touch $lockfile    critical-section    rm $lockfile else    echo "critical-section is already running" fi

What happens if someone kills your script while critical-section is running? The lockfile will be left there and your script won't run again until it's been deleted. The fix is to use:

if [ ! -e $lockfile ]; then    trap "rm -f $lockfile; exit" INT TERM EXIT    touch $lockfile    critical-section    rm $lockfile    trap - INT TERM EXIT else    echo "critical-section is already running" fi

Now when you kill the script it will delete the lock file too. Notice that we explicitly exit from the script at the end of trap command, otherwise the script will resume from the point that the signal was received.

A slightly more complicated problem is where you need to update a bunch of files and need the script to fail gracefully if there is a problem in the middle of the update. You want to be certain that something either happened correctly or that it appears as though it didn't happen at all.Say you had a script to add users.

add_to_passwd $user cp -a /etc/skel /home/$user chown $user /home/$user -R

There could be problems if you ran out of diskspace or someone killed the process. In this case you'd want the user to not exist and all their files to be removed.

rollback() {    del_from_passwd $user    if [ -e /home/$user ]; then       rm -rf /home/$user    fi    exit }  trap rollback INT TERM EXIT add_to_passwd $user cp -a /etc/skel /home/$user chown $user /home/$user -R trap - INT TERM EXIT

We needed to remove the trap at the end or the rollback function would have been called as we exited, undoing all the script's hard work.

No comments: