23

The GNU coreutils timeout command is extremely handy for certain scripting situations, allowing for using the output of a command if it is quick to run, and skipping it if it would take too long.

How can I approximate the basic behavior of timeout using only POSIX specified utilities?


(I'm thinking it may involve a combination of wait, sleep, kill and who knows what else, but perhaps I'm missing an easier approach.)

Wildcard
  • 36,499
  • 3
    See Timing out in a shell script, but I don't consider this a duplicate because I requested portability to pre-POSIX systems and I had the requirement of preserving stdin and stdout which some solutions involving background processes rule out. – Gilles 'SO- stop being evil' Apr 06 '16 at 00:15
  • command & pid=$! ; sleep 5 && kill $pid – Pandya Apr 06 '16 at 10:14
  • 1
    @Pandya doesn't that introduce a slight race condition, wherein if command finishes rapidly there is a slim chance of the pid being reused by another process starting up before the kill command runs? I wouldn't want that in production code.... – Wildcard Apr 06 '16 at 18:38
  • Can you explain more about the underlying problem you're trying to solve? Why not just compile the timeout program and use it? – James Youngman Apr 10 '16 at 22:53
  • You have got timelimittoo in Linux and FreeBSD, probably not POSIX – Rui F Ribeiro Apr 11 '16 at 19:03
  • 2
    @JamesYoungman, I'm guessing you're not very familiar with writing scripts for portability. Asking for a POSIX-compliant (or POSIX-specified) way of doing something implies that it's intended to be portable. Compiling source code into a binary is emphatically not portable, and, depending on your company security policies, you may not have a compiler installed at all on a production server. – Wildcard Apr 12 '16 at 00:32
  • @Wildcard nobody's suggesting you compile it on the machine where you'll install it, sheesh, use a deployment system. Also, answering my question by speculating about my level of clue is a decidedly unfriendly way to pursue a conversation. – James Youngman Apr 24 '16 at 09:31
  • @JamesYoungman, fair enough, but there is a distinct difference between "A POSIX-compliant portable shell script" and "A script that will work as long as you have XYZ package installed." – Wildcard Apr 24 '16 at 23:23
  • See also a number of answers here: https://unix.stackexchange.com/questions/43340/how-to-introduce-timeout-for-shell-scripting and another interesting solution here: https://stackoverflow.com/a/35512328/411282 – Joshua Goldberg Nov 08 '17 at 17:20

1 Answers1

6

My approach would be this one:

  • Execute command as background process 1

  • Execute "watchdog timer" as background process 2

  • Set up a handler to trap a termination signal in the parent shell

  • Wait for both processes to complete. The process that terminates first, sends the termination signal to the parent.

  • The parent's trap handler kills both background processes via job control (one of them has already terminated by definition, but that kill will be a harmless no-op because we are not using PIDs, see below)

I tried to circumvent the possible race condition addressed in the comments by using the shell's job control IDs (which would be unambiguous within this shell instance) to identify the background processes to kill, instead of system PIDs.

#!/bin/sh

TIMEOUT=$1
COMMAND='sleep 5'

function cleanup {
    echo "SIGTERM trap"
    kill %1 %2
}

trap cleanup SIGTERM

($COMMAND; echo "Command completed"; kill $$) &
(sleep $TIMEOUT; echo "Timeout expired"; kill $$) &

wait
echo "End of execution"

Result for TIMEOUT=10 (command terminates before watchdog):

$ ./timeout.sh 10
Command completed
SIGTERM trap
End of execution

Result for TIMEOUT=1 (watchdog terminates before command):

$ ./timeout.sh 1
Timeout expired
SIGTERM trap
End of execution

Result for TIMEOUT=5 (watchdog and command terminate "almost" simultaneously):

./tst.sh 5
Timeout expired
Command completed
SIGTERM trap
End of execution
Guido
  • 4,114
  • This is not POSIX compliant as (a) the function definition should be cleanup() { ...; } and (b) job control is a bash feature not specified by POSIX (though ksh and others have it also). Nice script, though! – Wildcard Oct 11 '17 at 04:59
  • You can also do better with timeout="$1"; shift and (exec "$@"; echo "Command completed"; kill $$) rather than re-wordsplitting the argument list. – Wildcard Oct 11 '17 at 05:00