Тайм-аут команды в bash без лишней задержки - PullRequest
254 голосов
/ 27 марта 2009

Этот ответ на Команда командной строки для автоматического уничтожения команды через определенное время

предлагает 1-строчный метод для тайм-аута длительной команды из командной строки bash:

( /path/to/slow command with options ) & sleep 5 ; kill $!

Но возможно, что данная "длительная" команда может завершиться раньше, чем время ожидания. (Давайте назовем это «обычно продолжительной, но иногда быстрой» командой или tlrbsf для развлечения.)

Так что у этого изящного подхода с 1 линией есть пара проблем. Во-первых, sleep не является условным, поэтому устанавливает нежелательную нижнюю границу времени, необходимого для завершения последовательности. Рассмотрим 30 с, 2 м или даже 5 м для сна, когда команда tlrbsf завершится через 2 секунды & mdash; крайне нежелательно. Во-вторых, kill является безусловным, поэтому эта последовательность попытается убить не запущенный процесс и скулить об этом.

Итак ...

Есть ли способ для тайм-аута обычно продолжительной, но иногда быстрой ( "tlrbsf" ) команды,

  • имеет реализацию bash (на другой вопрос уже есть ответы на Perl и C)
  • завершится в более раннем из двух: tlrbsf завершение программы или истекло время ожидания
  • не будет уничтожать несуществующие / не запущенные процессы (или, опционально: не будет жаловаться на неудачное уничтожение)
  • не обязательно должен быть 1-строчным
  • может работать под Cygwin или Linux

... и, для получения бонусных очков, запускает команду tlrbsf на переднем плане и любой «спящий» или дополнительный процесс в фоновом режиме, такой как stdin / stdout / stderr Команда tlrbsf может быть перенаправлена ​​так же, как если бы она была запущена напрямую?

Если это так, пожалуйста, поделитесь своим кодом. Если нет, объясните, пожалуйста, почему.

Я потратил некоторое время, пытаясь взломать вышеупомянутый пример, но я достиг предела своих навыков в bash.

Ответы [ 22 ]

475 голосов
/ 03 января 2011

Возможно, вы ищете команду timeout в coreutils. Так как это часть coreutils, технически это решение на C, но это все еще coreutils. info timeout для более подробной информации. Вот пример:

timeout 5 /path/to/slow/command with options
135 голосов
/ 27 марта 2009

Я думаю, это именно то, что вы просите:

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 "$@"
33 голосов
/ 15 июня 2012

Это решение работает независимо от режима мониторинга 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
19 голосов
/ 06 марта 2016

Вот, пожалуйста:

timeout --signal=SIGINT 10 /path/to/slow command with options

Вы можете изменить SIGINT и 10 по своему желанию;)

17 голосов
/ 15 августа 2010

Я предпочитаю "timelimit", который имеет пакет, по крайней мере, в Debian.

http://devel.ringlet.net/sysutils/timelimit/

Это немного лучше, чем "таймаут" для coreutils, потому что он печатает что-то, когда убивает процесс, а также отправляет SIGKILL через некоторое время по умолчанию.

15 голосов
/ 02 марта 2015

Вы можете сделать это полностью с 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`
)

Однако, как объяснено выше, вы не можете реэкспортировать переменные окружения в оболочку таким способом, естественно.

Edit:

Пример из реального мира: время ожидания __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.

9 голосов
/ 02 февраля 2010

См. Также скрипт http://www.pixelbeat.org/scripts/timeout, функциональность которого была интегрирована в более новые coreutils

8 голосов
/ 18 апреля 2015

timeout , вероятно, является первым подходом, который стоит попробовать. Вам может потребоваться уведомление или другая команда для выполнения, если она истекает. После долгих поисков и экспериментов я придумал сценарий 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
7 голосов
/ 27 марта 2009

Вроде хак, но это работает. Не работает, если у вас есть другие процессы переднего плана (пожалуйста, помогите мне исправить это!)

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
4 голосов
/ 24 октября 2018

Для тайм-аута slowcommand через 1 секунду:

timeout 1 slowcommand || echo "I failed, perhaps due to time out"

...