Выполнить функцию оболочки с тайм-аутом
Почему это работает
timeout 10s echo "foo bar" # foo bar
но это не будет
function echoFooBar {
echo "foo bar"
}
echoFooBar # foo bar
timeout 10s echoFooBar # timeout: failed to run command `echoFooBar': No such file or directory
и как я могу заставить его работать?
Ответы
Ответ 1
timeout
- это команда - поэтому она выполняется в подпроцессе вашей оболочки bash. Поэтому он не имеет доступа к вашим функциям, определенным в вашей текущей оболочке.
Приведена команда timeout
выполняется как подпроцесс таймаута - процесс grand-child вашей оболочки.
Вы можете быть смущены, потому что echo
является встроенной оболочкой и отдельной командой.
Что вы можете сделать, это поместить свою функцию в свой собственный script файл, chmod, чтобы он исполнялся, а затем выполнил его с помощью timeout
.
Альтернативно fork, выполняющий вашу функцию в под-оболочке - и в исходном процессе, отслеживает прогресс, убивая подпроцесс, если он занимает слишком много времени.
Ответ 2
Как Дуглас Лидер сказал, что вам нужен отдельный процесс таймаута для сигнала. Обходной путь путем экспорта функции в подоболочки и запуска подоболочки вручную.
export -f echoFooBar
timeout 10s bash -c echoFooBar
Ответ 3
Есть встроенная альтернатива, также запускающая подпроцесс bash shell:
timeout 10s bash <<EOT
function echoFooBar {
echo foo
}
echoFooBar
sleep 20
EOT
Ответ 4
Вы можете создать функцию, которая позволит вам делать то же самое, что и таймаут, но также и для других функций:
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
)
}
И может работать как ниже:
run_cmd "echoFooBar" 10
Примечание. Решение исходило из одного из моих вопросов:
Элегантное решение для выполнения тайм-аута для команд и функций bash
Ответ 5
если вы просто хотите добавить тайм-аут в качестве дополнительной опции для всего существующего script, вы можете проверить его на параметр тайм-аута, а затем заставить его вызывать его рекурсивно без этой опции.
example.sh:
#!/bin/bash
if [ "$1" == "-t" ]; then
timeout 1m $0 $2
else
#the original script
echo $1
sleep 2m
echo YAWN...
fi
работает script без таймаута:
$./example.sh -other_option # -other_option
# YAWN...
запуск с тайм-аутом в одну минуту:
$./example.sh -t -other_option # -other_option
Ответ 6
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
Ответ 7
Эта функция использует только встроенные
-
Возможно, стоит подумать о "$ *" вместо запуска $ @напрямую, в зависимости от ваших потребностей
-
Он запускает задание с командной строкой, указанной после первого аргумента, который является значением времени ожидания, и отслеживает pid задания
-
Он проверяет каждые 1 секунду, bash поддерживает тайм-ауты до 0,01, так что их можно настроить
-
Также, если вашему скрипту нужен стандартный ввод, read
должен полагаться на выделенный fd (exec {tofd}<> <(:)
)
-
Также вы можете настроить сигнал уничтожения (тот, который находится внутри цикла), который по умолчанию установлен на -15
, возможно, вы захотите -9
## forking is evil
timeout() {
to=$1; shift
[email protected] & 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
}
Ответ 8
Поместив мой комментарий к ответу Тиаго Лопо в более читабельную форму:
Я думаю, что более читабельно навязать тайм-аут самой последней подоболочки, так что нам не нужно оценивать строку, и весь сценарий может быть выделен как оболочка вашим любимым редактором. Я просто помещаю команды после появления подоболочки с eval
в shell-функцию (протестирована с zsh, но должна работать с bash):
timeout_child () {
trap -- "" SIGTERM
child=$!
timeout=$1
(
sleep $timeout
kill $child
) &
wait $child
}
Пример использования:
( while true; do echo -n.; sleep 0.1; done) & timeout_child 2
И таким образом он также работает с функцией оболочки (если она работает в фоновом режиме):
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; )
Ответ 9
У меня есть небольшая модификация ответа @Tiago Lopo, которая может обрабатывать команды с несколькими аргументами. Я также протестировал решение TauPan, но оно не работает, если вы используете его несколько раз в сценарии, в то время как Tiago делает.
function timeout_cmd {
local arr
local cmd
local timeout
arr=( "[email protected]" )
# 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"
)
}
Вот полнофункциональный скрипт, который вы можете использовать для проверки функции выше:
$ ./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].
Например, вы можете запустить так:
$ ./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 "[email protected]" )
}
else
function wrap_run() { "[email protected]"; }
fi
# Execute a shell function with timeout
# https://stackoverflow.com/a/24416732/2377454
function timeout_cmd {
local arr
local cmd
local timeout
arr=( "[email protected]" )
# 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
Ответ 10
Этот лайнер выйдет из сеанса Bash через 10 секунд.
$ TMOUT=10 && echo "foo bar"