2016-07-06
How to handle SIGWINCH in an almquist shell
Last month I asked, is SIGWINCH in shells broken?
I explained how the shell allows you to create signal handlers, but takes care of all the dangerous bits of signal handling for you, so unlike in C or C++ you can run any code from within a signal handler. One exception was SIGWINCH, a signal that caused my shell script to terminate about 50% of the time.
I concluded my article with an example of how I handle the signal, which was met with some people telling me that they cannot reproduce my problem:
# Record shell size changes
trap "trap '' WINCH;winch_trapped=1" WINCH
…
# Handle window changes
if [ -n "$winch_trapped" ]; then
# Reinstall the trap
winch_trapped=
trap "trap '' WINCH;winch_trapped=1" WINCH
# Redraw the current output
redraw
fi
As it would turn out, I should have RTFM more carefully.
A better example
This code snippet, as it turned out, did not contain the actual problem. It was implied (but not stated) that the signal handling code happens in some kind of loop.
Said loop looks somewhat like this:
# Record shell size changes
trap "trap '' WINCH;winch_trapped=1" WINCH
while read -r line; do
# Handle window changes
if [ -n "$winch_trapped" ]; then
# Reinstall the trap
winch_trapped=
trap "trap '' WINCH;winch_trapped=1" WINCH
# Redraw the current output
redraw
fi
case "$line" in
…
esac
done
Tracking it Down
This, it turned out, was important.
At this point, I would usually describe the debugging process, but by now I do not remember everything that I've done.
Fixing it
In the end it turned out that there is a difference in the behaviour between ash and bash. The explanation can be found in the manual page of ash:
The exit status is 0 on success, 1 on end of file, between 2 and
128 if an error occurs and greater than 128 if a trapped signal
interrupts read.
So in case of a signal trap, ash executes the trap and has read return a value greater 128, which is equivalent to read(2) setting errno=EINTR.
This behaviour is not shared by bash, which seems to resume an interrupted read transparently. It also has different rules for return values:
… The return code is zero, unless end-of-file is
encountered, read times out (in which case the return code is
greater than 128), a variable assignment error (such as assign-
ing to a readonly variable) occurs, or an invalid file descrip-
tor is supplied as the argument to -u.
So unless the -t flag is used bash's read does not return values greater than 128, which makes developing working code easy:
# Record shell size changes
trap "trap '' WINCH;winch_trapped=1" WINCH
while true; do
read -r line
retval=$?
if [ $retval -gt 128 ]; then
# Resume interrupted read
continue
elif [ $retval -ne 0 ]; then
# Read failed
break
fi
# Handle window changes
if [ -n "$winch_trapped" ]; then
# Reinstall the trap
winch_trapped=
trap "trap '' WINCH;winch_trapped=1" WINCH
# Redraw the current output
redraw
fi
case "$line" in
…
esac
done
Somewhere there is a lesson to learn in there. It is mostly in the missing part about how to debug this kind of problem, though.