Тайм-аут команды в bash без лишней задержки
Это ответ на команду командной строки для автоматического уничтожения команды через определенное время
предлагает 1-строчный метод для тайм-аута длительной команды из командной строки bash:
( /path/to/slow command with options ) & sleep 5 ; kill $!
Но возможно, что данная "длительная" команда может завершиться раньше, чем время ожидания. (Давайте назовем это "обычно продолжительной, но иногда быстрой" командой или tlrbsf для забавы.)
Так что у этого изящного подхода с 1 линией есть пара проблем. Во-первых, sleep
не является условным, поэтому устанавливает нежелательную нижнюю границу времени, необходимого для завершения последовательности. Рассмотрим 30 с, 2 м или даже 5 м для сна, когда команда tlrbsf заканчивается через 2 секунды - крайне нежелательно. Во-вторых, kill
является безусловным, поэтому эта последовательность попытается убить не запущенный процесс и скулить об этом.
Так...
Есть ли способ для тайм-аута обычно длительной, но иногда быстрой ("tlrbsf") команды, которая
- имеет реализацию bash (на другой вопрос уже есть ответы на Perl и C)
- будет завершен в более раннем из двух: завершение программы tlrbsf или истекло время ожидания
- не будет уничтожать несуществующие / не запущенные процессы (или, опционально: не будет жаловаться на неудачное уничтожение)
- не должен быть 1-лайнер
- может работать под Cygwin или Linux
... и, для получения бонусных баллов, запускает команду tlrbsf на переднем плане и любой "спящий" или дополнительный процесс в фоновом режиме, так что stdin / stdout / stderr команды tlrbsf может быть перенаправлен так же, как если бы он был бежать напрямую?
Если это так, пожалуйста, поделитесь своим кодом. Если нет, пожалуйста, объясните почему.
Я потратил некоторое время, пытаясь взломать вышеупомянутый пример, но я достигаю предела своих навыков в bash.
24 ответа
Я думаю, что это именно то, что вы просите:
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 <Dmitry.Golovashkin@sas.com>
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 "$@"
Вы, наверное, ищете timeout
команда в coreutils. Поскольку это часть coreutils, технически это решение на C, но это все еще coreutils. info timeout
Больше подробностей. Вот пример:
timeout 5 /path/to/slow/command with options
Это решение работает независимо от режима мониторинга bash. Вы можете использовать правильный сигнал для завершения вашей_команды
#!/bin/sh
( your_command ) & pid=$!
( sleep $TIMEOUT && kill -HUP $pid ) 2>/dev/null & watcher=$!
wait $pid 2>/dev/null && pkill -HUP -P $watcher
Наблюдатель убивает your_command после заданного времени ожидания; скрипт ожидает медленной задачи и завершает работу наблюдателя. Обратите внимание, что wait
не работает с процессами, которые являются потомками другой оболочки.
Примеры:
- your_command выполняется более 2 секунд и был прерван
ваша_команда прервана
( 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
- ваша_команда завершена до истечения времени ожидания (20 секунд)
ваша_команда завершена
( 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
Чтобы тайм-аут slowcommand
через 1 секунду:
timeout 1 slowcommand || echo "I failed, perhaps due to time out"
Там вы идете:
timeout --signal=SIGINT 10 /path/to/slow command with options
Вы можете изменить SIGINT
а также 10
по вашему желанию;)
Вы можете сделать это полностью с bash 4.3
и выше:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; r=$?; kill -9 `jobs -p`; exit $r; ) }
- Пример:
_timeout 5 longrunning_command args
- Пример:
{ _timeout 5 producer || echo KABOOM $?; } | consumer
- Пример:
producer | { _timeout 5 consumer1; consumer2; }
Пример:
{ while date; do sleep .3; done; } | _timeout 5 cat | less
Нужен Bash 4.3 для
wait -n
- Дает 137, если команда была убита, иначе возвращаемое значение команды.
- Работает для труб. (Вам не нужно идти на передний план здесь!)
- Работает также с внутренними командами или функциями оболочки.
- Работает в подоболочке, поэтому нет экспорта переменных в текущую оболочку, извините.
Если вам не нужен код возврата, это можно сделать еще проще:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; kill -9 `jobs -p`; ) }
Заметки:
Строго говоря, вам не нужно
;
в; )
Однако это делает вещь более последовательной; }
-дело. Иset +b
вероятно, может быть оставлено в стороне, но лучше в безопасности, чем потом сожалеть.За исключением
--forground
(возможно) вы можете реализовать все вариантыtimeout
поддерживает.--preserve-status
Это немного сложно, хотя. Это оставлено в качестве упражнения для читателя;)
Этот рецепт может быть использован "естественно" в оболочке (так же естественно, как и для flock fd
):
(
set +b
sleep 20 &
{
YOUR SHELL CODE HERE
} &
wait -n
kill `jobs -p`
)
Однако, как объяснялось выше, вы не можете естественным образом реэкспортировать переменные окружения в оболочку.
Редактировать:
Пример из реального мира: время вышло __git_ps1
в случае, если это занимает слишком много времени (для таких вещей, как медленные SSHFS-ссылки):
eval "__orig$(declare -f __git_ps1)" && __git_ps1() { ( git() { _timeout 0.3 /usr/bin/git "$@"; }; _timeout 0.3 __orig__git_ps1 "$@"; ) }
Edit2: исправление. Я заметил, что exit 137
не нужен и делает _timeout
ненадежный в то же время.
Edit3: git
твердолобый, поэтому для удовлетворительной работы нужен двойной трюк.
Edit4: Забыли _
во-первых _timeout
для реального примера GIT.
Я предпочитаю "timelimit", который имеет пакет, по крайней мере, в Debian.
http://devel.ringlet.net/sysutils/timelimit/
Это немного приятнее, чем у coreutils "timeout", потому что он печатает что-то, когда убивает процесс, а также отправляет SIGKILL через некоторое время по умолчанию.
Тайм-аут, вероятно, первый подход, чтобы попробовать. Вам может потребоваться уведомление или другая команда для выполнения, если она истекает. После долгих поисков и экспериментов я придумал сценарий bash:
if
timeout 20s COMMAND_YOU_WANT_TO_EXECUTE;
timeout 20s AS_MANY_COMMANDS_AS_YOU_WANT;
then
echo 'OK'; #if you want a positive response
else
echo 'Not OK';
AND_ALTERNATIVE_COMMANDS
fi
Смотрите также http://www.pixelbeat.org/scripts/timeout скрипт, функциональность которого была интегрирована в более новые coreutils
Вроде хак, но это работает. Не работает, если у вас есть другие процессы переднего плана (пожалуйста, помогите мне исправить это!)
sleep TIMEOUT & SPID=${!}; (YOUR COMMAND HERE; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}
На самом деле, я думаю, что вы можете отменить это, отвечая вашим "бонусным" критериям:
(YOUR COMMAND HERE & SPID=${!}; (sleep TIMEOUT; kill ${SPID}) & CPID=${!}; fg 1; kill ${CPID}) < asdf > fdsa
Простой скрипт с понятностью кода. Сохранить /usr/local/bin/run
:
#!/bin/bash
# run
# Run command with timeout $1 seconds.
# Timeout seconds
timeout_seconds="$1"
shift
# PID
pid=$$
# Start timeout
(
sleep "$timeout_seconds"
echo "Timed out after $timeout_seconds seconds"
kill -- -$pid &>/dev/null
) &
timeout_pid=$!
# Run
"$@"
# Stop timeout
kill $timeout_pid &>/dev/null
Тайм-аут команды, которая выполняется слишком долго:
$ run 2 sleep 10
Timed out after 2 seconds
Terminated
$
Заканчивается сразу для команды, которая завершает:
$ run 10 sleep 2
$
Если вы уже знаете название программы (предположим, program
) прекратить после тайм-аута (например, 3
секунд) я могу предложить простое и несколько грязное альтернативное решение:
(sleep 3 && killall program) & ./program
Это прекрасно работает, если я вызываю тестовые процессы с помощью системных вызовов.
OS X пока не использует bash 4 и не имеет / usr / bin / timeout, так что вот функция, которая работает в OS X без home-brew или macports, которая похожа на /usr/bin/timeout (основана на Tino's ответ). Проверка параметров, помощь, использование и поддержка других сигналов - упражнение для читателя.
# implement /usr/bin/timeout only if it doesn't exist
[ -n "$(type -p timeout 2>&1)" ] || function timeout { (
set -m +b
sleep "$1" &
SPID=${!}
("${@:2}"; RETVAL=$?; kill ${SPID}; exit $RETVAL) &
CPID=${!}
wait %1
SLEEPRETVAL=$?
if [ $SLEEPRETVAL -eq 0 ] && kill ${CPID} >/dev/null 2>&1 ; then
RETVAL=124
# When you need to make sure it dies
#(sleep 1; kill -9 ${CPID} >/dev/null 2>&1)&
wait %2
else
wait %2
RETVAL=$?
fi
return $RETVAL
) }
Есть также cratimeout
Martin Cracauer (написано на C для систем Unix и Linux).
# cf. http://www.cons.org/cracauer/software.html
# usage: cratimeout timeout_in_msec cmd args
cratimeout 5000 sleep 1
cratimeout 5000 sleep 600
cratimeout 5000 tail -f /dev/null
cratimeout 5000 sh -c 'while sleep 1; do date; done'
Вот версия, которая не основана на порождении дочернего процесса - мне нужен был автономный скрипт, в который встроены эти функции. Это также делает интервал частичного опроса, так что вы можете опрашивать быстрее. Тайм-аут был бы предпочтительным - но я застрял на старом сервере
# wait_on_command <timeout> <poll interval> command
wait_on_command()
{
local timeout=$1; shift
local interval=$1; shift
$* &
local child=$!
loops=$(bc <<< "($timeout * (1 / $interval)) + 0.5" | sed 's/\..*//g')
((t = loops))
while ((t > 0)); do
sleep $interval
kill -0 $child &>/dev/null || return
((t -= 1))
done
kill $child &>/dev/null || kill -0 $child &>/dev/null || return
sleep $interval
kill -9 $child &>/dev/null
echo Timed out
}
slow_command()
{
sleep 2
echo Completed normally
}
# wait 1 sec in 0.1 sec increments
wait_on_command 1 0.1 slow_command
# or call an external command
wait_on_command 1 0.1 sleep 10
Если вы хотите сделать это в своем скрипте, поместите это туда:
parent=$$
( sleep 5 && kill -HUP $parent ) 2>/dev/null &
Сама команда тайм-аута имеет --foreground
вариант. Это позволяет команде взаимодействовать с пользователем, "когда тайм-аут не запускается непосредственно из приглашения оболочки".
timeout --foreground the_command its_options
Я думаю, что спрашивающий должен был знать об очень очевидном решении проблемы. timeout
команда, но по этой причине попросила альтернативное решение. timeout
у меня не сработало, когда я вызвал его с помощью popen
, т.е. "не напрямую из оболочки". Однако позвольте мне не предполагать, что это могло быть причиной в случае допрашивающего. Взгляните на его справочную страницу.
Моя проблема была, возможно, немного другой: я запускаю команду через ssh на удаленной машине и хочу уничтожить оболочку и дочерние элементы, если команда зависает.
Теперь я использую следующее:
ssh server '( sleep 60 && kill -9 0 ) 2>/dev/null & my_command; RC=$? ; sleep 1 ; pkill -P $! ; exit $RC'
Таким образом, команда возвращает 255, когда был тайм-аут или код возврата команды в случае успеха
Обратите внимание, что процессы уничтожения в ssh-сессии обрабатываются не так, как в интерактивной оболочке. Но вы также можете использовать опцию -t для ssh для выделения псевдотерминала, так что он действует как интерактивная оболочка
Мне предложили проблему с сохранением контекста оболочки и разрешением тайм-аутов, единственная проблема с ним заключается в том, что он останавливает выполнение скрипта по тайм-ауту - но это соответствует потребностям, которые мне представили
#!/usr/bin/env bash
safe_kill()
{
ps aux | grep -v grep | grep $1 >/dev/null && kill ${2:-} $1
}
my_timeout()
{
typeset _my_timeout _waiter_pid _return
_my_timeout=$1
echo "Timeout($_my_timeout) running: $*"
shift
(
trap "return 0" USR1
sleep $_my_timeout
echo "Timeout($_my_timeout) reached for: $*"
safe_kill $$
) &
_waiter_pid=$!
"$@" || _return=$?
safe_kill $_waiter_pid -USR1
echo "Timeout($_my_timeout) ran: $*"
return ${_return:-0}
}
my_timeout 3 cd scripts
my_timeout 3 pwd
my_timeout 3 true && echo true || echo false
my_timeout 3 false && echo true || echo false
my_timeout 3 sleep 10
my_timeout 3 pwd
с выходами:
Timeout(3) running: 3 cd scripts
Timeout(3) ran: cd scripts
Timeout(3) running: 3 pwd
/home/mpapis/projects/rvm/rvm/scripts
Timeout(3) ran: pwd
Timeout(3) running: 3 true
Timeout(3) ran: true
true
Timeout(3) running: 3 false
Timeout(3) ran: false
false
Timeout(3) running: 3 sleep 10
Timeout(3) reached for: sleep 10
Terminated
конечно, я предполагаю, что там был режиссер scripts
#! /bin/bash
timeout=10
interval=1
delay=3
(
((t = timeout)) || :
while ((t > 0)); do
echo "$t"
sleep $interval
# Check if the process still exists.
kill -0 $$ 2> /dev/null || exit 0
((t -= interval)) || :
done
# Be nice, post SIGTERM first.
{ echo SIGTERM to $$ ; kill -s TERM $$ ; sleep $delay ; kill -0 $$ 2> /dev/null && { echo SIGKILL to $$ ; kill -s KILL $$ ; } ; }
) &
exec "$@"
Очень упрощенный способ:
# command & sleep 5; pkill -9 -x -f "command"
с помощью pkill (опция -f) вы можете уничтожить вашу конкретную команду с аргументами или указать -n, чтобы избежать уничтожения старого процесса.
В 99% случаев ответ НЕ для реализации какой-либо логики тайм-аута. Логика тайм-аута практически в любой ситуации является красным предупреждающим признаком того, что что-то не так и должно быть исправлено.
Ваш процесс иногда зависает или прерывается через n секунд? Затем выясните причину и исправьте это.
Кроме того, для правильного решения проблемы strager вам нужно использовать wait "$SPID" вместо fg 1, так как в сценариях у вас нет управления заданиями (и пытаться включить его глупо). Более того, fg 1 основывается на том факте, что вы ранее не запускали никаких других заданий в сценарии, что является неверным предположением.
Опираясь на ответ @loup...
Если вы хотите отключить процесс и отключить вывод задания /pid, выполните:
( (sleep 1 && killall program 2>/dev/null) &) && program --version
Это помещает фоновый процесс в подоболочку, поэтому вы не видите выходных данных задания.
У меня есть работа cron, которая вызывает скрипт php, и иногда он застревает на скрипте php. Это решение было идеальным для меня.
Я использую:
scripttimeout -t 60 /script.php