Reputation: 13335
Why would this work
timeout 10s echo "foo bar" # foo bar
but this wouldn't
function echoFooBar {
echo "foo bar"
}
echoFooBar # foo bar
timeout 10s echoFooBar # timeout: failed to run command `echoFooBar': No such file or directory
and how can I make it work?
Upvotes: 106
Views: 166219
Reputation: 11
This small modification to TauPan's answer adds some useful protection. If the child process that is being waited for has already exited before the sleep $timeout completes. The kill command attempts to kill a process that no longer exists. This is probably harmless, but there is no absolute guarantee that the same PID has not been re-assigned. To obviate this, a quick check is done to test that the child PID exists and that its parent is the shell it was forked from. Also trying to kill a non-existent process generates errors which if not suppressed can easily fill up logs.
I also used a more aggressive kill -9. This is the only way to kill a process that is blocking not on the shell command but instead from the file system eg. read < named_pipe
.
A consequence of this is that the kill -9 $child
command send its kill signal asynchronously to the process and hence generates a message into the calling shell. This can be suppressed by re-directing the wait $child > /dev/null 2>&1
. With obvious consequences for debugging.
#!/bin/bash
function child_timeout () {
child=$!
timeout=$1
(
#trap -- "" SIGINT
sleep $timeout
if [ $(ps -o pid= -o comm= --ppid $$ | grep -o $child) ]; then
kill -9 $child
fi
) &
wait $child > /dev/null 2>&1
}
( tail -f /dev/null ) & child_timeout 10
Upvotes: 1
Reputation: 1777
This one liner will exit your Bash session after 10s
$ TMOUT=10 && echo "foo bar"
Upvotes: -1
Reputation: 861
I have a slight modification of @Tiago Lopo's answer that can handle commands with multiple arguments. I've also tested TauPan's solution, but it does not work if you use it multiple times in a script, while Tiago's does.
function timeout_cmd {
local arr
local cmd
local timeout
arr=( "$@" )
# timeout: first arg
# cmd: the other args
timeout="${arr[0]}"
cmd=( "${arr[@]:1}" )
(
eval "${cmd[@]}" &
child=$!
echo "child: $child"
trap -- "" SIGTERM
(
sleep "$timeout"
kill "$child" 2> /dev/null
) &
wait "$child"
)
}
Here's a fully functional script thant you can use to test the function above:
$ ./test_timeout.sh -h
Usage:
test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT]
test_timeout.sh -h
Test timeout_cmd function.
Options:
-n Dry run, do not actually sleep.
-r REPEAT Reapeat everything multiple times [default: 1].
-s SLEEP_TIME Sleep for SLEEP_TIME seconds [default: 5].
-t TIMEOUT Timeout after TIMEOUT seconds [default: no timeout].
For example you cnal launch like this:
$ ./test_timeout.sh -r 2 -s 5 -t 3
Try no: 1
- Set timeout to: 3
child: 2540
-> retval: 143
-> The command timed out
Try no: 2
- Set timeout to: 3
child: 2593
-> retval: 143
-> The command timed out
Done!
#!/usr/bin/env bash
#shellcheck disable=SC2128
SOURCED=false && [ "$0" = "$BASH_SOURCE" ] || SOURCED=true
if ! $SOURCED; then
set -euo pipefail
IFS=$'\n\t'
fi
#################### helpers
function check_posint() {
local re='^[0-9]+$'
local mynum="$1"
local option="$2"
if ! [[ "$mynum" =~ $re ]] ; then
(echo -n "Error in option '$option': " >&2)
(echo "must be a positive integer, got $mynum." >&2)
exit 1
fi
if ! [ "$mynum" -gt 0 ] ; then
(echo "Error in option '$option': must be positive, got $mynum." >&2)
exit 1
fi
}
#################### end: helpers
#################### usage
function short_usage() {
(>&2 echo \
"Usage:
test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT]
test_timeout.sh -h"
)
}
function usage() {
(>&2 short_usage )
(>&2 echo \
"
Test timeout_cmd function.
Options:
-n Dry run, do not actually sleep.
-r REPEAT Reapeat everything multiple times [default: 1].
-s SLEEP_TIME Sleep for SLEEP_TIME seconds [default: 5].
-t TIMEOUT Timeout after TIMEOUT seconds [default: no timeout].
")
}
#################### end: usage
help_flag=false
dryrun_flag=false
SLEEP_TIME=5
TIMEOUT=-1
REPEAT=1
while getopts ":hnr:s:t:" opt; do
case $opt in
h)
help_flag=true
;;
n)
dryrun_flag=true
;;
r)
check_posint "$OPTARG" '-r'
REPEAT="$OPTARG"
;;
s)
check_posint "$OPTARG" '-s'
SLEEP_TIME="$OPTARG"
;;
t)
check_posint "$OPTARG" '-t'
TIMEOUT="$OPTARG"
;;
\?)
(>&2 echo "Error. Invalid option: -$OPTARG.")
(>&2 echo "Try -h to get help")
short_usage
exit 1
;;
:)
(>&2 echo "Error.Option -$OPTARG requires an argument.")
(>&2 echo "Try -h to get help")
short_usage
exit 1
;;
esac
done
if $help_flag; then
usage
exit 0
fi
#################### utils
if $dryrun_flag; then
function wrap_run() {
( echo -en "[dry run]\\t" )
( echo "$@" )
}
else
function wrap_run() { "$@"; }
fi
# Execute a shell function with timeout
# https://stackoverflow.com/a/24416732/2377454
function timeout_cmd {
local arr
local cmd
local timeout
arr=( "$@" )
# timeout: first arg
# cmd: the other args
timeout="${arr[0]}"
cmd=( "${arr[@]:1}" )
(
eval "${cmd[@]}" &
child=$!
echo "child: $child"
trap -- "" SIGTERM
(
sleep "$timeout"
kill "$child" 2> /dev/null
) &
wait "$child"
)
}
####################
function sleep_func() {
local secs
local waitsec
waitsec=1
secs=$(($1))
while [ "$secs" -gt 0 ]; do
echo -ne "$secs\033[0K\r"
sleep "$waitsec"
secs=$((secs-waitsec))
done
}
command=("wrap_run" \
"sleep_func" "${SLEEP_TIME}"
)
for i in $(seq 1 "$REPEAT"); do
echo "Try no: $i"
if [ "$TIMEOUT" -gt 0 ]; then
echo " - Set timeout to: $TIMEOUT"
set +e
timeout_cmd "$TIMEOUT" "${command[@]}"
retval="$?"
set -e
echo " -> retval: $retval"
# check if (retval % 128) == SIGTERM (== 15)
if [[ "$((retval % 128))" -eq 15 ]]; then
echo " -> The command timed out"
fi
else
echo " - No timeout"
"${command[@]}"
retval="$?"
fi
done
echo "Done!"
exit 0
Upvotes: 1
Reputation: 131
Putting my comment to Tiago Lopo's answer into more readable form:
I think it's more readable to impose a timeout on the most recent subshell, this way we don't need to eval a string and the whole script can be highlighted as shell by your favourite editor. I simply put the commands after the subshell with eval
has spawned into a shell-function (tested with zsh, but should work with bash):
timeout_child () {
trap -- "" SIGTERM
child=$!
timeout=$1
(
sleep $timeout
kill $child
) &
wait $child
}
Example usage:
( while true; do echo -n .; sleep 0.1; done) & timeout_child 2
And this way it also works with a shell function (if it runs in the background):
print_dots () {
while true
do
sleep 0.1
echo -n .
done
}
> print_dots & timeout_child 2
[1] 21725
[3] 21727
...................[1] 21725 terminated print_dots
[3] + 21727 done ( sleep $timeout; kill $child; )
Upvotes: 1
Reputation: 623
This function uses only builtins
Maybe consider evaling "$*" instead of running $@ directly depending on your needs
It starts a job with the command string specified after the first arg that is the timeout value and monitors the job pid
It checks every 1 seconds, bash supports timeouts down to 0.01 so that can be tweaked
Also if your script needs stdin, read
should rely on a dedicated fd (exec {tofd}<> <(:)
)
Also you might want to tweak the kill signal (the one inside the loop) which is default to -15
, you might want -9
## forking is evil
timeout() {
to=$1; shift
$@ & local wp=$! start=0
while kill -0 $wp; do
read -t 1
start=$((start+1))
if [ $start -ge $to ]; then
kill $wp && break
fi
done
}
Upvotes: 1
Reputation: 2547
As Douglas Leeder said you need a separate process for timeout to signal to. Workaround by exporting function to subshells and running subshell manually.
export -f echoFooBar
timeout 10s bash -c echoFooBar
Upvotes: 105
Reputation: 51
function foo(){
for i in {1..100};
do
echo $i;
sleep 1;
done;
}
cat <( foo ) # Will work
timeout 3 cat <( foo ) # Will Work
timeout 3 cat <( foo ) | sort # Wont work, As sort will fail
cat <( timeout 3 cat <( foo ) ) | sort -r # Will Work
Upvotes: 5
Reputation: 7959
You can create a function which would allow you to do the same as timeout but also for other functions:
function run_cmd {
cmd="$1"; timeout="$2";
grep -qP '^\d+$' <<< $timeout || timeout=10
(
eval "$cmd" &
child=$!
trap -- "" SIGTERM
(
sleep $timeout
kill $child 2> /dev/null
) &
wait $child
)
}
And could run as below:
run_cmd "echoFooBar" 10
Note: The solution came from one of my questions: Elegant solution to implement timeout for bash commands and functions
Upvotes: 14
Reputation: 783
There's an inline alternative also launching a subprocess of bash shell:
timeout 10s bash <<EOT
function echoFooBar {
echo foo
}
echoFooBar
sleep 20
EOT
Upvotes: 33
Reputation: 1371
if you just want to add timeout as an additional option for the entire existing script, you can make it test for the timeout-option, and then make it call it self recursively without that option.
example.sh:
#!/bin/bash
if [ "$1" == "-t" ]; then
timeout 1m $0 $2
else
#the original script
echo $1
sleep 2m
echo YAWN...
fi
running this script without timeout:
$./example.sh -other_option # -other_option
# YAWN...
running it with a one minute timeout:
$./example.sh -t -other_option # -other_option
Upvotes: 9
Reputation: 53310
timeout
is a command - so it is executing in a subprocess of your bash shell. Therefore it has no access to your functions defined in your current shell.
The command timeout
is given is executed as a subprocess of timeout - a grand-child process of your shell.
You might be confused because echo
is both a shell built-in and a separate command.
What you can do is put your function in it's own script file, chmod it to be executable, then execute it with timeout
.
Alternatively fork, executing your function in a sub-shell - and in the original process, monitor the progress, killing the subprocess if it takes too long.
Upvotes: 79