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?

7 Answers7

37

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.

  • 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. – Gilles 'SO- stop being evil' 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 'SO- stop being evil' 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
  • @Gilles: See my edit. Not pretty and I can't tell about portability. – Stéphane Gimenez Aug 16 '11 at 21:07
  • 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 'SO- stop being evil' Aug 20 '11 at 19:05
  • Is there an easy way to adapt this method such that the output-producing command outputs to the invoking terminal in real-time? In the above example, output is only visible after timeout/completion via the variable whose value was set to the output of the double-program-group magic. – Zac B Jun 13 '12 at 15:54
  • @Zac: Don't tell me you also need to run commands on 20th century systems? You can just remove the variable assignation if you don't want to collect the output. – Stéphane Gimenez Jun 13 '12 at 19:12
  • Sure, but if I run { { echo boo; kill 0; } | { sleep 2; kill 0; } } 3>&1, I don't see "boo" printed to stdout. – Zac B Jun 13 '12 at 19:31
  • @Zac: You forgot the 1>&3 redirection after the echo. – Stéphane Gimenez Jun 13 '12 at 19:36
  • D'oh! Also, for brevity's sake, you can reverse the ordering of the two internal program groups, and then you don't have to do any redirection. Gorgeous solution, btw. – Zac B Jun 14 '12 at 15:32
  • 2
    @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
  • Unfortunately it does not work on OSX, probably their bash is too old: GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin14) Copyright (C) 2007 Free Software Foundation, Inc. – Gabor Csardi Feb 13 '15 at 15:40
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).

geekosaur
  • 32,047
  • 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 'SO- stop being evil' Apr 05 '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 'SO- stop being evil' Apr 05 '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 05 '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 05 '11 at 23:18
  • I can't background “whatever”, I need its output. Thanks for the info about $!. – Gilles 'SO- stop being evil' Apr 05 '11 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 <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
bahamat
  • 39,666
  • 4
  • 75
  • 104
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.

Soren
  • 443
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"
pete
  • 1
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
Earl
  • 1
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &