Software Development Services

Trap 0

29 June 2012

Files used in this post available at https://github.com/danwatford/trap0-tests.git

In build scripts for complex systems we sometimes need to perform operations that cause side-effects on the hosting system.

Take build systems that generate disk images as an example. Under Linux you can use tools like qemu-nbd to take a file and present it as a Network Block Device (NBD). After partitioning the NBD, generating filesystems on them, and then mounting filesystems we are left with a lot of baggage should the build now fail. This baggage may leak resources or prevent future builds from executing. Enter Trap Zero.

Listing 1 - simple_t0.lib - Simple Trap Zero Handler

in_t0() {
    echo Adding to trap 0: $@
    _T0="$@; $_T0"
    trap "set -x; $_T0" 0
}

launch_and_check() {
    "$@"
    result=$?
    echo "Result $result for: $@"
    [ $result -eq 0 ] || exit 1
}

Listing 1 shows a simple function (in_t0) to execute the given parameters as a single command in trap zero. The function can be called multiple times and the commands will be executed in the reverse order in trap zero to when they were added.

Listing 2 - test_t0_1.sh

#!/bin/sh
. ./simple_t0.lib

echo Echo 1
in_t0 echo Echo 2
echo Echo 3

To demonstrate the trap 0 functionality place the files from Listings 1 and 2 in the same directory. Executing test_t0_1.sh should give output similar to:

Echo 1
Adding to trap 0: echo Echo 2
Echo 3
+ echo Echo 2
Echo 2

“Echo 3” is printed before “Echo 2”. This is because the echo Echo 2 statement is only executed as part of the Trap 0 handler upon exit from the script.

This was a very simplistic and virtually useless example, and the implementation of the trap 0 handler can be improved on significantly too. A scenario where use of the trap 0 handler will be useful is when working with chroot. Depending on what you need to do inside the chroot you may need to bind the /proc, /dev and /sys directories into the chroot directory. Listing 3 shows how this may be done, utilising the trap 0 handler to cleanly remove the binds at exit from the script. This will handle the case where and error causes the script to exit.

Listing 3 - test_t0_2.sh - Using trap 0 for chroot work

#!/bin/bash
. ./simple_t0.lib
DIR=/tmp/foomoo
mkdir -p $DIR

# Mount proc, sys and dev in the target directory ready for a chroot.
launch_and_check mkdir $DIR/proc $DIR/sys $DIR/dev
in_t0 rmdir $DIR/proc $DIR/sys $DIR/dev

launch_and_check sudo mount --bind /proc $DIR/proc
in_t0 sudo umount $DIR/proc

# Simulate an error condition if $DIR/error_marker exists.
set -e
[ ! -e $DIR/error_marker ] || false

launch_and_check sudo mount --bind /sys $DIR/sys
in_t0 sudo umount $DIR/sys

launch_and_check sudo mount --bind /dev $DIR/dev
in_t0 sudo umount $DIR/dev

echo chroot would go here.

In Listing 3, lines 7, 10, 17 and 20 prepare the environment ready for the chroot, and immediately following each of these lines we add a command to undo their behaviour. For example, line 7 creates three directories and line 8 adds a command to delete these directories to the trap 0 handler.

Listing 4 - Output from running test_t0_2.sh

Result 0 for: mkdir /tmp/foomoo/proc /tmp/foomoo/sys /tmp/foomoo/dev
Adding to trap 0: rmdir /tmp/foomoo/proc /tmp/foomoo/sys /tmp/foomoo/dev
Result 0 for: sudo mount --bind /proc /tmp/foomoo/proc
Adding to trap 0: sudo umount /tmp/foomoo/proc
Result 0 for: sudo mount --bind /sys /tmp/foomoo/sys
Adding to trap 0: sudo umount /tmp/foomoo/sys
Result 0 for: sudo mount --bind /dev /tmp/foomoo/dev
Adding to trap 0: sudo umount /tmp/foomoo/dev
chroot would go here.
+ sudo umount /tmp/foomoo/dev
+ sudo umount /tmp/foomoo/sys
+ sudo umount /tmp/foomoo/proc
+ rmdir /tmp/foomoo/proc /tmp/foomoo/sys /tmp/foomoo/dev

Listing 4 shows the output of running test_t2_2.sh. After the chroot has executed we see three unmount commands executed in reverse order compared to when added to trap 0, followed by the rmdir. The changes to the environment have been unwound, but this doesn’t hasn’t demonstrated anything different to what could have been achieved by simply writing the unmount statements after the chroot.

The power of this approach really comes from coping with errors during execution. Trying running test_t0_2.sh after creating file /tmp/foomoo/error_marker. Listing 5 shows the expected output.

Listing 5 - Output from running test_t0_2.sh with /tmp/foomoo/error_marker present

Result 0 for: mkdir /tmp/foomoo/proc /tmp/foomoo/sys /tmp/foomoo/dev
Adding to trap 0: rmdir /tmp/foomoo/proc /tmp/foomoo/sys /tmp/foomoo/dev
Result 0 for: sudo mount --bind /proc /tmp/foomoo/proc
Adding to trap 0: sudo umount /tmp/foomoo/proc
+ sudo umount /tmp/foomoo/proc
+ rmdir /tmp/foomoo/proc /tmp/foomoo/sys /tmp/foomoo/dev

In this case line 15 of test_t0_2.sh exits the script causing the trap 0 handler to kick in, unwinding the changes made so far.

Limitations The in_t0 implementation presented in Listing 1 should be improved on and shall be addressed in a future post. The current implementation uses a single shell variable to hold all commands to be executed in the trap handler. If the contents of this variable grow it could hit a size limit. Also, it would be a good idea to have a non-successful execution of any of the command in the trap 0 handler cause a non-zero exit code to be returned by the script. This means any problems with the unmounts in the example above could then be communicated out to the caller.