Shell Script Timeout – Timing Out in a Shell Script

shellsignalstimeout

I have a shell script that's reading from standard input. In rare circumstances, there will be no one ready to provide input, and the script must time out. In case of timeout, the script must execute some cleanup code. What's the best way to do that?

This script must be very portable, including to 20th century unix systems without a C compiler and to embedded devices running busybox, so Perl, bash, any compiled language, and even the full POSIX.2 can't be relied on. In particular, $PPID, read -t and perfectly POSIX-compliant traps are not available. Writing to a temporary file is also excluded; the script might run even if all filesystems are mounted read-only.

Just to make things more difficult, I also want the script to be reasonably fast when it doesn't time out. In particular, I also use the script in Windows (mainly in Cygwin), where fork and exec are particularly low, so I want to keep their use to a minimum.

In a nutshell, I have

trap cleanup 1 2 3 15
foo=`cat`

and I want to add a timeout. I can't replace cat with the read built-in. In case of timeout, I want to execute the cleanup function.


Background: this script is guessing the encoding of the terminal by printing some 8-bit characters and comparing the cursor position before and after. The beginning of the script tests that stdout is connected to a supported terminal, but sometimes the environment is lying (e.g. plink sets TERM=xterm even if it's called with TERM=dumb). The relevant part of the script looks like this:

text='Éé'  # UTF-8; shows up as Ãé on a latin1 terminal
csi='␛['; dsr_cpr="${csi}6n"; dsr_ok="${csi}5n"  # ␛ is an escape character
stty_save=`stty -g`
cleanup () { stty "$stty_save"; }
trap 'cleanup; exit 120' 0 1 2 3 15     # cleanup code
stty eol 0 eof n -echo                # Input will end with `0n`
# echo-n is a function that outputs its argument without a newline
echo-n "$dsr_cpr$dsr_ok"              # Ask the terminal to report the cursor position
initial_report=`tr -dc \;0123456789`  # Expect ␛[42;10R␛[0n for y=42,x=10
echo-n "$text$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
cleanup
# Compute and return initial_x - final_x

How can I modify the script so that if tr hasn't read any input after 2 seconds, it is killed and the script executes the cleanup function?

Best Answer

What about this:

foo=`{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1`

That is: run the output-producing command and sleep in the same process group, a process group just for them. Whichever command returns first kills the whole process group.

Would anyone wonder: Yes, the pipe is not used; it's bypassed using the redirections. The sole purpose of it is to have the shell run the two process in the same process group.


As Gilles pointed out in his comment, this won't work in a shell script because the script process would be killed along with the two subprocesses.

One way¹ to force a command to run in a separate process group is to start a new interactive shell:

#!/bin/sh
foo=`sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null`
[ -n "$foo" ] && echo got: "$foo" || echo timeouted

But there might be caveats with this (e.g. when stdin is not a tty?). The stderr redirection is there to get rid of the "Terminated" message when the interactive shell is killed.

Tested with zsh,bash and dash. But what about oldies?

B98 suggests the following change, working on Mac OS X, with GNU bash 3.2.57, or Linux with dash:

foo=`sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }'`


1. other than setsid which appears to be non-standard.

Related Question