Элегантное решение для реализации тайм-аута для команд и функций bash

#bash

#bash

Вопрос:

Я написал функцию для выполнения команд, которая принимает два аргумента 1-й команды 2-й тайм-аут в секундах:

 #! /bin/bash

function run_cmd {
    cmd="$1"; timeout="$2"
    grep -qP "^d $" <<< "$timeout" || timeout=10

    stderrfile=$(readlink /proc/$$/fd/2)
    exec 2<amp;-

    exitfile=/tmp/exit_$(date  %s.%N)
    (eval "$cmd";echo $? > $exitfile) amp;

    start=$(date  %s)
    while true; do
        pid=$(jobs -l | awk '/Running/{print $2}')
        if [ -n "$pid" ]; then
            now=$(date  %s)
            running=$(($now - $start))
            if [ "$running" -ge "$timeout" ];then
                kill -15 "$pid"
                exit=1
            fi
            sleep 1
        else
            break
        fi

    done 
    test -n "$exit" || exit=$(cat $exitfile)
    rm $exitfile
    exec 2>$stderrfile              
    return "$exit"
}


function sleep5 {
    sleep 5
    echo "I slept 5"
    return 2
}

run_cmd sleep5 "6" 
run_cmd sleep5 "3"
echo "hi" >amp;2 
  

Функция работает нормально, но я не уверен, что это элегантное решение, я хотел бы узнать об альтернативах для следующего

  1. Мне приходится сохранять статус выхода в файле: (eval "$cmd";echo $? > $exitfile)
  2. Я закрываю и снова открываю STDERR: exec 2<amp;- and exec 2>$stderrfile

Я закрываю STDERR, потому что не смог избежать сообщения при завершении команды:

test.sh: line 3: 32323 Terminated ( eval "$cmd"; echo $? > $exitfile )

PS: Я знаю timeout и expect , но они не будут работать для функций.

Комментарии:

1. Какова полная цель скрипта.

2. @Jidder цель состоит в том, чтобы иметь возможность прерывать команды и функции, но статус выхода очень важен, поскольку я также буду реализовывать функцию повтора.

Ответ №1:

Возможно, это соответствует вашим потребностям. Я изменил сигнатуру вызова, чтобы можно было избежать использования eval .

 # Usage: run_with_timeout N cmd args...
#    or: run_with_timeout cmd args...
# In the second case, cmd cannot be a number and the timeout will be 10 seconds.
run_with_timeout () { 
    local time=10
    if [[ $1 =~ ^[0-9] $ ]]; then time=$1; shift; fi
    # Run in a subshell to avoid job control messages
    ( "$@" amp;
      child=$!
      # Avoid default notification in non-interactive shell for SIGTERM
      trap -- "" SIGTERM
      ( sleep $time
        kill $child 2> /dev/null ) amp;
      wait $child
    )
}
  

Пример, показывающий состояние выхода:

 $ sleep_and_exit() { sleep ${1:-1}; exit ${2:-0}; }

$ time run_with_timeout 1 sleep_and_exit 3 0; echo $?

real    0m1.007s
user    0m0.003s
sys     0m0.006s
143

$ time run_with_timeout 3 sleep_and_exit 1 0; echo $?

real    0m1.007s
user    0m0.003s
sys     0m0.008s
0

$ time run_with_timeout 3 sleep_and_exit 1 7; echo $?

real    0m1.006s
user    0m0.001s
sys     0m0.006s
7
  

Как показано, статус выхода будет run_with_timeout статусом выхода выполняемой команды, если она не была прервана по истечении времени ожидания, и в этом случае он будет равен 143 (128 15).

Примечание: Если вы установили большой тайм-аут и / или запустили forkbomb, вы можете перерабатывать pid достаточно быстро, чтобы kill-child уничтожил неправильный процесс.

Комментарии:

1. 1 Мне нравится подход, но я все равно получаю: test3.sh: line 9: 14075 Terminated "$@"

2. @Tiago: прогоны, которые я показываю в примере, в точности соответствуют тому, как они отображаются на моем терминале. Какую оболочку вы используете? Также: вы скопировали его в скрипт вместо того, чтобы оставить его как функцию?

3. попробуйте run_with_timeout с функцией, подобной sleep5 из моего скрипта.

4. @Tiago: я использовал именно ваш sleep5 и получил тот же результат, что и ожидалось.

5. @lx42.de : В этом нет необходимости. Внешний (...) завершится, как только wait завершится, потому что процесс, запущенный как завершенный, или потому, что "$@" он был убит kill после sleep . Как только внешняя (...) завершается, внутренняя (...) завершается, даже если она все еще спит, потому что время ожидания было больше времени выполнения. Вы правы, это kill -KILL было бы более определенно, но kill -TERM (сигнал по умолчанию) лучше, если вам нужно выполнить некоторую очистку в обработчике терминов.

Ответ №2:

Если вы хотите управлять функциями, вы можете использовать обработчик ловушек (например, в C)

 $ trap 'break' 15
$ echo $$; while :; do :; done; echo 'endlessloop terminated'
5168
endlessloop terminated
$
  

Если вы вводите kill -15 5168 в другой оболочке, программа прерывается и печатает endlessloop terminated

Если вы создаете подпроцесс, пожалуйста, позаботьтесь о четырех дополнительных вещах

  1. если подпроцесс завершается задолго до перехода в спящий режим, это приводит к длительному переходу в спящий режим. Поэтому лучше не прерывать режим ожидания и продолжать проверять несколько раз. Например, лучше сделать 360 снов по 10 секунд, чем спать 3600 с = 1 час. Поскольку режим ожидания может заполнить вашу таблицу процессов до предела. (Или вам нужно прервать режим ожидания, как только $cmd завершится.)

  2. если процесс не реагирует на обычное завершение, вы можете добавить дополнительное kill -9 через несколько секунд после этого.

  3. если вам нужно возвращаемое значение процесса, то вам необходимо расширить программу с помощью оболочки, которая доставляет возвращаемое значение в файл / fifo.

  4. если вам нужен вывод stdout / stderr процесса, … file/fifo .

Все эти вещи покрываются временным ограничением программы на C.

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

 $ timelimit
timelimit: using defaults: warntime=3600, warnsig=15, killtime=120, killsig=9
timelimit: usage: timelimit [-pq] [-S ksig] [-s wsig] [-T ktime] [-t wtime] command
  

Эта программа имеет несколько преимуществ:

  • он проверяет, все ли еще выполняется процесс и не завершился ли он во время ожидания для уничтожения
  • if сначала отправляет soft killsignal и жесткий сигнал -9, если это не работает
  • он распространяет (Option -p) уровень возврата ($?), Чтобы вы могли использовать его для своих целей.

Комментарии:

1. Я удивлен вашим комментарием, потому что вы отметили ответ rici как правильный ответ. Хорошо, позвольте мне написать несколько слов, чтобы расширить мой ответ.

2. Я принял Rici как правильный ответ 5 месяцев назад 🙂 1 за ваши усилия.

Ответ №3:

Я полагаю, что у меня есть элегантное решение, основанное на ответе @rici (который я принял), и решил, что поделюсь конечным результатом, я также добавил функцию повтора, которая была реальной целью.

 function run_cmd { 
    cmd="$1"; timeout="$2";
    grep -qP '^d $' <<< $timeout || timeout=10

    ( 
        eval "$cmd" amp;
        child=$!
        trap -- "" SIGTERM 
        (       
                sleep $timeout
                kill $child 
        ) > /dev/null 2>amp;1 amp;     
        wait $child
    )
}

function retry { 
        cmd=$1; timeout=$2; tries=$3; interval=$4
        grep -qP '^d $' <<< $timeout || timeout=10
        grep -qP '^d $' <<< $tries || tries=3 
        grep -qP '^d $' <<< $interval || interval=3
        for ((c=1; c <= $tries; c  )); do
                run_cmd "$cmd" "$timeout" amp;amp; return
                sleep $interval
        done    
        return 1
}
  

Функция повтора принимает 4 аргумента:

  1. Команда
  2. Тайм-аут
  3. Попытки
  4. Интервал

Его можно выполнить, как показано ниже:

retry "some_command_or_function arg1 arg2 .." 5 2 10

Комментарии:

1. Вы бы использовали эту функцию вместо grep? isint () { [ «$1» != «» — а «${1//[0-9-]}» = «» ]; }

2. ( @lx42.de ) Более эффективная, хотя, возможно, и менее читаемая альтернатива: timeout=${2:-10};timeout=${timeout/*[^[:digit:]]*/10} . Функция glob ( *[^[:digit:]]*/ ) полностью сопоставляет любую строку, содержащую не цифру, поэтому замена на 10 ( /10 ) происходит точно, если значение $2 имеет не цифру. Первое {2:-10} необходимо вставить 10 в качестве значения по умолчанию. Лично я предпочитаю сигнализировать об ошибке для недопустимых значений, но YMMV.