Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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?

share|improve this question
    
See also How to introduce timeout for shell scripting? for less portable solutions –  Gilles Jul 18 '12 at 23:37

5 Answers 5

up vote 17 down vote accepted
+100

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 subprocess.

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?


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

share|improve this answer
1  
I quite like this, I hadn't thought of using a process group that way. It's pretty simple and there's no race condition. I need to test it for portability some more, but this looks like a winner. –  Gilles Aug 14 '11 at 18:50
    
Unfortunately, this fails spectacularly as soon as I put this in a script: the pipeline in the command substitution does not run in its own process group, and kill 0 ends up killing the script's caller as well. Is there a portable way to force the pipeline into its own process group? –  Gilles Aug 16 '11 at 14:09
    
@Gilles: Eek! couldn't find a way to setprgp() without setsid for now :-( –  Stéphane Gimenez Aug 16 '11 at 15:06
1  
I really like the trick, so I'm awarding the bounty. It seems to work on Linux, I haven't had time to test it on other systems yet. –  Gilles Aug 20 '11 at 19:05
1  
@Zac: It's not possible to reverse the ordering in the original case, because only the first process get access to stdin. –  Stéphane Gimenez Jun 14 '12 at 19:46

Another way to run the pipeline in its own process group is to run sh -c '....' in a pseudo-terminal using the script command (which implicitly applies the setsid function).

#!/bin/sh
stty -echo -onlcr
# GNU script
foo=`script -q -c 'sh -c "{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }" 3>&1 2>/dev/null' /dev/null`
# FreeBSD script
#foo=`script -q /dev/null sh -c '{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }' 3>&1 2>/dev/null`
stty echo onlcr
echo "foo: $foo"


# alternative without: stty -echo -onlcr
# cr=`printf '\r'`
# foo=`script -q -c 'sh -c "{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null"' /dev/null | sed -e "s/${cr}$//" -ne 'p;N'`  # GNU
# foo=`script -q /dev/null sh -c '{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null' | sed -e "s/${cr}$//" -ne 'p;N'`  # FreeBSD
# echo "foo: $foo"
share|improve this answer
    
Not portable enough. I don't have script on some old Unix boxen, nor on many embedded Linux. –  Gilles Feb 18 at 17:50

What about (ab)use NC for this

Like;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

Or rolled up into a single command line;

   $ foo=`nc -l 0 2222 | nc -w 5 0 2222`

The last version, although looking stange actually does work when I test it on a linux system -- my best guess is that it should work on any system, and if not a variation of output redirection may solve for portability. benefit here is that no background processes are involved.

share|improve this answer
    
Not portable enough. I don't have nc on some old Unix boxen, nor on many embedded Linux. –  Gilles Feb 18 at 17:51

Although coretuils as of version 7.0 includes a timeout command you've mentioned some environments that won't have it. Fortunately pixelbeat.org has a timeout script written sh.

I've used it before on several occasions and it works very well.

http://www.pixelbeat.org/scripts/timeout (Note: The script below has been slightly modified from the one on pixelbeat.org, see the comments below this answer.)

#!/bin/sh

# Execute a command with a timeout

# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <brad@footle.org>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <jasan@x31.com>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <jasan@x31.com>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value
share|improve this answer
    
It seems that this does not allow to retrieve the output of the command. See "I can't background “whatever“" comment by Gilles in the other answer. –  Stéphane Gimenez Aug 14 '11 at 18:27
    
Ah, interesting. I've modified the script (so it won't match the one on pixelbeat anymore) to redirect /dev/stdin into the command. It seems to work in my testing. –  bahamat Aug 14 '11 at 20:33
    
This doesn't work to have the command read from standard input, except (oddly) in bash. </dev/stdin is a no-op. </dev/tty would enable it to read from the terminal, which is good enough for my use case. –  Gilles Aug 15 '11 at 18:10
    
@Giles: cool, I'll make that update. –  bahamat Aug 15 '11 at 18:42
    
This can't work without considerably more effort: I need to retrieve the command's output, and I can't do that if the command is in the background. –  Gilles Aug 16 '11 at 14:12
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

You're already trapping 15 (the numeric version of SIGTERM, which is what kill sends unless told otherwise), so you should already be good to go. That said, if you are looking at pre-POSIX, be aware that shell functions may not exist either (they came from System V's shell).

share|improve this answer
    
It's not that simple. If you trap the signal, many shells (dash, bash, pdksh, zsh… probably all except ATT ksh) ignore it while they're waiting for cat to exit. I've already experimented a little but not found anything I'm satisfied with so far. –  Gilles Apr 5 '11 at 23:11
    
On portability: thankfully I have functions everywhere. I'm not sure about $!, I think some of those machines I use rarely don't have job control, is $! universally available? –  Gilles Apr 5 '11 at 23:13
    
@Gilles: Hm, yeh. I recall some truly hideous and bash-specific stuff I did once involving abuse of -o monitor. I was thinking in terms of truly ancient shells when I wrote that (it worked in v7). That said, I think you can do either of two other things: (1) background "whatever" and wait $!, or (2) also send SIGCLD/SIGCHLD... but on old enough machines the latter is either nonexistent or nonportable (the former is System III/V, the latter BSD, and V7 had neither). –  geekosaur Apr 5 '11 at 23:17
    
@Gilles: $! goes back to V7 at least, and certainly predates a sh-like that knew anything about job control (in fact, for a long time /bin/sh on BSD didn't do job control; you had to run csh to get it — but $! was there). –  geekosaur Apr 5 '11 at 23:18
    
I can't background “whatever”, I need its output. Thanks for the info about $!. –  Gilles Apr 5 '11 at 23:21

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.