Timeout a command in bash without unnecessary delay
Timeout a command in bash without unnecessary delay
Question
This answer to Command line command to auto-kill a command after a certain amount of time
proposes a 1-line method to timeout a long-running command from the bash command line:
( /path/to/slow command with options ) & sleep 5 ; kill $!
But it's possible that a given "long-running" command may finish earlier than the timeout.
(Let's call it a "typically-long-running-but-sometimes-fast" command, or tlrbsf for fun.)
So this nifty 1-liner approach has a couple of problems.
First, the sleep
isn't conditional, so that sets an undesirable lower bound on the time taken for the sequence to finish. Consider 30s or 2m or even 5m for the sleep, when the tlrbsf command finishes in 2 seconds — highly undesirable.
Second, the kill
is unconditional, so this sequence will attempt to kill a non-running process and whine about it.
So...
Is there a way to timeout a typically-long-running-but-sometimes-fast ("tlrbsf") command that
- has a bash implementation (the other question already has Perl and C answers)
- will terminate at the earlier of the two: tlrbsf program termination, or timeout elapsed
- will not kill non-existing/non-running processes (or, optionally: will not complain about a bad kill)
- doesn't have to be a 1-liner
- can run under Cygwin or Linux
... and, for bonus points
- runs the tlrbsf command in the foreground
- any 'sleep' or extra process in the background
such that the stdin/stdout/stderr of the tlrbsf command can be redirected, same as if it had been run directly?
If so, please share your code. If not, please explain why.
I have spent awhile trying to hack the aforementioned example but I'm hitting the limit of my bash skills.
Accepted Answer
I think this is precisely what you are asking for:
http://www.bashcookbook.com/bashinfo/source/bash-4.0/examples/scripts/timeout3
#!/bin/bash
#
# The Bash shell script executes a command with a time-out.
# Upon time-out expiration SIGTERM (15) is sent to the process. If the signal
# is blocked, then the subsequent SIGKILL (9) terminates it.
#
# Based on the Bash documentation example.
# Hello Chet,
# please find attached a "little easier" :-) to comprehend
# time-out example. If you find it suitable, feel free to include
# anywhere: the very same logic as in the original examples/scripts, a
# little more transparent implementation to my taste.
#
# Dmitry V Golovashkin <[email protected]>
scriptName="${0##*/}"
declare -i DEFAULT_TIMEOUT=9
declare -i DEFAULT_INTERVAL=1
declare -i DEFAULT_DELAY=1
# Timeout.
declare -i timeout=DEFAULT_TIMEOUT
# Interval between checks if the process is still alive.
declare -i interval=DEFAULT_INTERVAL
# Delay between posting the SIGTERM signal and destroying the process by SIGKILL.
declare -i delay=DEFAULT_DELAY
function printUsage() {
cat <<EOF
Synopsis
$scriptName [-t timeout] [-i interval] [-d delay] command
Execute a command with a time-out.
Upon time-out expiration SIGTERM (15) is sent to the process. If SIGTERM
signal is blocked, then the subsequent SIGKILL (9) terminates it.
-t timeout
Number of seconds to wait for command completion.
Default value: $DEFAULT_TIMEOUT seconds.
-i interval
Interval between checks if the process is still alive.
Positive integer, default value: $DEFAULT_INTERVAL seconds.
-d delay
Delay between posting the SIGTERM signal and destroying the
process by SIGKILL. Default value: $DEFAULT_DELAY seconds.
As of today, Bash does not support floating point arithmetic (sleep does),
therefore all delay/time values must be integers.
EOF
}
# Options.
while getopts ":t:i:d:" option; do
case "$option" in
t) timeout=$OPTARG ;;
i) interval=$OPTARG ;;
d) delay=$OPTARG ;;
*) printUsage; exit 1 ;;
esac
done
shift $((OPTIND - 1))
# $# should be at least 1 (the command to execute), however it may be strictly
# greater than 1 if the command itself has options.
if (($# == 0 || interval <= 0)); then
printUsage
exit 1
fi
# kill -0 pid Exit code indicates if a signal may be sent to $pid process.
(
((t = timeout))
while ((t > 0)); do
sleep $interval
kill -0 $$ || exit 0
((t -= interval))
done
# Be nice, post SIGTERM first.
# The 'exit 0' below will be executed if any preceeding command fails.
kill -s SIGTERM $$ && kill -0 $$ || exit 0
sleep $delay
kill -s SIGKILL $$
) 2> /dev/null &
exec "[email protected]"
Popular Answer
You are probably looking for the timeout
command in coreutils. Since it's a part of coreutils, it is technically a C solution, but it's still coreutils. info timeout
for more details.
Here's an example:
timeout 5 /path/to/slow/command with options
Read more... Read less...
This solution works regardless of bash monitor mode. You can use the proper signal to terminate your_command
#!/bin/sh
( your_command ) & pid=$!
( sleep $TIMEOUT && kill -HUP $pid ) 2>/dev/null & watcher=$!
wait $pid 2>/dev/null && pkill -HUP -P $watcher
The watcher kills your_command after given timeout; the script waits for the slow task and terminates the watcher. Note that wait
does not work with processes which are children of a different shell.
Examples:
- your_command runs more than 2 seconds and was terminated
your_command interrupted
( sleep 20 ) & pid=$!
( sleep 2 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
echo "your_command finished"
pkill -HUP -P $watcher
wait $watcher
else
echo "your_command interrupted"
fi
- your_command finished before the timeout (20 seconds)
your_command finished
( sleep 2 ) & pid=$!
( sleep 20 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
echo "your_command finished"
pkill -HUP -P $watcher
wait $watcher
else
echo "your_command interrupted"
fi
There you go:
timeout --signal=SIGINT 10 /path/to/slow command with options
you may change the SIGINT
and 10
as you desire ;)
You can do this entirely with bash 4.3
and above:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; r=$?; kill -9 `jobs -p`; exit $r; ) }
- Example:
_timeout 5 longrunning_command args
- Example:
{ _timeout 5 producer || echo KABOOM $?; } | consumer
- Example:
producer | { _timeout 5 consumer1; consumer2; }
Example:
{ while date; do sleep .3; done; } | _timeout 5 cat | less
Needs Bash 4.3 for
wait -n
- Gives 137 if the command was killed, else the return value of the command.
- Works for pipes. (You do not need to go foreground here!)
- Works with internal shell commands or functions, too.
- Runs in a subshell, so no variable export into the current shell, sorry.
If you do not need the return code, this can be made even simpler:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; kill -9 `jobs -p`; ) }
Notes:
Strictly speaking you do not need the
;
in; )
, however it makes thing more consistent to the; }
-case. And theset +b
probably can be left away, too, but better safe than sorry.Except for
--forground
(probably) you can implement all variantstimeout
supports.--preserve-status
is a bit difficult, though. This is left as an exercise for the reader ;)
This recipe can be used "naturally" in the shell (as natural as for flock fd
):
(
set +b
sleep 20 &
{
YOUR SHELL CODE HERE
} &
wait -n
kill `jobs -p`
)
However, as explained above, you cannot re-export environment variables into the enclosing shell this way naturally.
Edit:
Real world example: Time out __git_ps1
in case it takes too long (for things like slow SSHFS-Links):
eval "__orig$(declare -f __git_ps1)" && __git_ps1() { ( git() { _timeout 0.3 /usr/bin/git "[email protected]"; }; _timeout 0.3 __orig__git_ps1 "[email protected]"; ) }
Edit2: Bugfix. I noticed that exit 137
is not needed and makes _timeout
unreliable at the same time.
Edit3: git
is a die-hard, so it needs a double-trick to work satisfyingly.
Edit4: Forgot a _
in the first _timeout
for the real world GIT example.
I prefer "timelimit", which has a package at least in debian.
http://devel.ringlet.net/sysutils/timelimit/
It is a bit nicer than the coreutils "timeout" because it prints something when killing the process, and it also sends SIGKILL after some time by default.
To timeout the slowcommand
after 1 second:
timeout 1 slowcommand || echo "I failed, perhaps due to time out"
To determine whether the command timed out or failed for its own reasons, check whether the status code is 124:
# ping for 3 seconds, but timeout after only 1 second
timeout 1 ping 8.8.8.8 -w3
EXIT_STATUS=$?
if [ $EXIT_STATUS -eq 124 ]
then
echo 'Process Timed Out!'
else
echo 'Process did not timeout. Something else went wrong.'
fi
exit $EXIT_STATUS
Note that when the exit status is 124, you don't know whether it timed out due to your timeout
command, or whether the command itself terminated due to some internal timeout logic of its own and then returned 124. You can safely assume in either case, though, that a timeout of some kind happened.