61

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?

1

7 Answers 7

37
+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 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.

12
  • 2
    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. Aug 14, 2011 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? Aug 16, 2011 at 14:09
  • @Gilles: Eek! couldn't find a way to setprgp() without setsid for now :-( Aug 16, 2011 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. Aug 20, 2011 at 19:05
  • 2
    @Zac: It's not possible to reverse the ordering in the original case, because only the first process get access to stdin. Jun 14, 2012 at 19:46
6
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).

5
  • 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. Apr 5, 2011 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? Apr 5, 2011 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, 2011 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, 2011 at 23:18
  • I can't background “whatever”, I need its output. Thanks for the info about $!. Apr 5, 2011 at 23:21
5

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 <[email protected]>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <[email protected]>
#                       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 <[email protected]>
#                       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
6
  • 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. Aug 14, 2011 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, 2011 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. Aug 15, 2011 at 18:10
  • @Giles: cool, I'll make that update.
    – bahamat
    Aug 15, 2011 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. Aug 16, 2011 at 14:12
3

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.

1
  • Not portable enough. I don't have nc on some old Unix boxen, nor on many embedded Linux. Feb 18, 2014 at 17:51
0

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"
1
  • Not portable enough. I don't have script on some old Unix boxen, nor on many embedded Linux. Feb 18, 2014 at 17:50
0

The answer in https://unix.stackexchange.com/a/18711 is very nice.

I wanted to achieve a similar outcome, but without having to explicitly invoke the shell again because I wanted to invoke existing shell functions.

Using bash, it is possible to do the following:

eval 'set -m ; ( ... ) ; '"$(set +o)"

So suppose I already have a shell function f:

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

Executing this I see:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .