Тайм-аут в сценарии оболочки

53

У меня есть сценарий оболочки, который читает со стандартного ввода . В редких случаях никто не будет готов предоставить данные, и сценарий должен отключиться . В случае тайм-аута скрипт должен выполнить некоторый код очистки. Какой лучший способ сделать это?

Этот сценарий должен быть очень переносимым , в том числе для Unix-систем 20-го века без компилятора C и для встраиваемых устройств, работающих под управлением busybox, поэтому нельзя полагаться на Perl, bash, любой скомпилированный язык и даже полную версию POSIX.2. В частности, $PPID, read -tи отлично POSIX-совместимые ловушки не доступны. Запись во временный файл также исключена; скрипт может работать, даже если все файловые системы смонтированы только для чтения.

Просто чтобы сделать вещи более сложными, я также хочу, чтобы сценарий был достаточно быстрым, когда он не истекает. В частности, я также использую скрипт в Windows (в основном в Cygwin), где fork и exec особенно низки, поэтому я хочу свести их использование к минимуму.

Короче говоря, у меня есть

trap cleanup 1 2 3 15
foo=`cat`

и я хочу добавить тайм-аут. Я не могу заменить catна readвстроенный. В случае тайм-аута я хочу выполнить cleanupфункцию.


Справочная информация: этот скрипт угадывает кодировку терминала, печатая некоторые 8-битные символы и сравнивая положение курсора до и после. В начале скрипта проверяется, что stdout подключен к поддерживаемому терминалу, но иногда среда врет (например, plinkустанавливает, TERM=xtermдаже если онTERM=dumb вызывается с помощью ). Соответствующая часть скрипта выглядит так:

text='Éé'  # UTF-8; shows up as Ãé on a latin1 terminal
csi='␛['; dsr_cpr="${csi}6n"; dsr_ok="${csi}5n"  # ␛ is an escape character
stty_save=`stty -g`
cleanup () { stty "$stty_save"; }
trap 'cleanup; exit 120' 0 1 2 3 15     # cleanup code
stty eol 0 eof n -echo                # Input will end with `0n`
# echo-n is a function that outputs its argument without a newline
echo-n "$dsr_cpr$dsr_ok"              # Ask the terminal to report the cursor position
initial_report=`tr -dc \;0123456789`  # Expect ␛[42;10R␛[0n for y=42,x=10
echo-n "$text$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
cleanup
# Compute and return initial_x - final_x

Как я могу изменить сценарий так, чтобы, если trчерез 2 секунды не было прочитано ни одного ввода, он был уничтожен и сценарий выполнил cleanupфункцию?

Жиль "ТАК - перестань быть злым"
источник
2
Смотрите также Как ввести время ожидания для сценариев оболочки? для менее портативных решений
Жиль "ТАК - перестань быть злым"

Ответы:

33

Как насчет этого:

foo=`{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1`

То есть: запустите выходную команду и sleepв той же группе процессов группу процессов только для них. Какая бы команда не вернулась первой, она уничтожает всю группу процессов.

Кто-нибудь задумался: да, труба не используется; это обойдено с помощью перенаправлений. Единственная цель состоит в том, чтобы оболочка запускала два процесса в одной и той же группе процессов.


Как отметил Жиль в своем комментарии, это не будет работать в сценарии оболочки, потому что процесс сценария будет уничтожен вместе с двумя подпроцессами.

Один из способов заставить команду работать в отдельной группе процессов - это запустить новую интерактивную оболочку:

#!/bin/sh
foo=`sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null`
[ -n "$foo" ] && echo got: "$foo" || echo timeouted

Но с этим могут быть оговорки (например, когда stdin не tty?). Перенаправление stderr позволяет избавиться от сообщения «Прекращено», когда интерактивная оболочка уничтожена.

Проверено с zsh, bashи dash. Но как насчет старых?

B98 предлагает следующее изменение, работающее на Mac OS X с GNU bash 3.2.57 или Linux с dash:

foo=`sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }'`

-
1. кроме того, setsidчто кажется нестандартным.

Стефан Хименес
источник
1
Мне очень нравится это, я не думал об использовании группы процессов таким образом. Это довольно просто и нет условий гонки. Мне нужно еще раз проверить его на мобильность, но это выглядит как победитель.
Жиль "ТАК - перестань быть злым"
К сожалению, это неожиданно завершается неудачей, как только я помещаю это в сценарий: конвейер в подстановке команд не запускается в своей собственной группе процессов и в kill 0конечном итоге также убивает вызывающего сценарий. Существует ли переносимый способ принудительного запуска конвейера в собственную группу процессов?
Жиль "ТАК - перестань быть злым"
@ Жиль: Eek! не могу найти способ setprgp()без setsidпока :-(
Стефан Гименес
1
Мне очень нравится этот трюк, поэтому я присуждаю награду. Кажется, он работает в Linux, у меня еще не было времени проверить его на других системах.
Жиль "ТАК - перестань быть злым"
2
@Zac: Невозможно изменить порядок в исходном случае, потому что только первый процесс получает доступ к stdin.
Стефан Гименес
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Вы уже в ловушке 15 (числовая версия SIGTERM, которая killотправляет, если не указано иное), так что вы уже должны быть готовы. Тем не менее, если вы смотрите на pre-POSIX, имейте в виду, что функции оболочки также могут не существовать (они пришли из оболочки System V).

geekosaur
источник
Это не так просто. Если вы перехватываете сигнал, многие оболочки (dash, bash, pdksh, zsh… вероятно, все, кроме ATT ksh) игнорируют его, ожидая catвыхода. Я уже немного поэкспериментировал, но пока не нашел ничего, чем доволен.
Жиль "ТАК - перестань быть злым"
На переносимость: к счастью, у меня есть функции везде. Я не уверен в этом $!, я думаю, что некоторые из тех машин, которые я использую редко, не имеют контроля работы, $!доступны ли они повсеместно?
Жиль "ТАК - прекрати быть злым"
@ Жиль: Хм, да. Я помню некоторые действительно отвратительные и bashспецифические вещи, которые я когда-то делал, связанные со злоупотреблениями -o monitor. Когда я писал об этом, я думал о действительно древних оболочках (это работало в v7). Тем не менее, я думаю, что вы можете сделать одну из двух других вещей: (1) фон «что угодно» и wait $!(2) также отправить SIGCLD/ SIGCHLD... но на достаточно старых машинах последний либо не существует, либо не переносим (первый - System III / V, последний BSD и V7 не имели ни того, ни другого).
geekosaur
@Gilles: $!восходит, по крайней мере, к V7, и, безусловно, предшествует тому, shчто знал что-то о контроле за работой (на самом деле, долгое время /bin/shна BSD не контролировали работу; нужно было бежать, cshчтобы его получить - но $!был там) ).
geekosaur
Я не могу справиться с «чем угодно», мне нужен его вывод. Спасибо за информацию о $!.
Жиль "ТАК - перестань быть злым"
4

Хотя в coretuils версии 7.0 есть команда timeout, вы упомянули некоторые среды, в которых ее нет. К счастью, на pixelbeat.org написан сценарий тайм-аута sh.

Я использовал его раньше несколько раз, и он работает очень хорошо.

http://www.pixelbeat.org/scripts/timeout ( Примечание . Сценарий, приведенный ниже, был немного изменен по сравнению с сценарием на pixelbeat.org, см. комментарии ниже к этому ответу.)

#!/bin/sh

# Execute a command with a timeout

# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <brad@footle.org>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <jasan@x31.com>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <jasan@x31.com>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value
bahamat
источник
Похоже, что это не позволяет получить выходные данные команды. См. Комментарий Жиля «Я не могу справиться с« каким бы то ни было »» в другом ответе.
Стефан Гименес
Ах, интересно. Я изменил скрипт (чтобы он больше не совпадал со сценарием pixelbeat), чтобы перенаправить / dev / stdin в команду. Кажется, работает в моем тестировании.
Багамат
Это не работает для чтения команды из стандартного ввода, за исключением (как ни странно) в bash. </dev/stdinэто неоперация. </dev/ttyпозволит ему читать с терминала, что достаточно для моего случая использования.
Жиль "ТАК - перестань быть злым"
@ Джайлс: круто, я сделаю это обновление.
Багамат
Это не может работать без значительных дополнительных усилий: мне нужно получить выходные данные команды, и я не могу этого сделать, если команда находится в фоновом режиме.
Жиль "ТАК - перестань быть злым"
3

Как насчет (ab) использовать NC для этого

Подобно;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

Или свернут в одну командную строку;

   $ foo=`nc -l 0 2222 | nc -w 5 0 2222`

Последняя версия, хотя выглядит странно, действительно работает, когда я тестирую ее в системе linux - я думаю, что она должна работать в любой системе, и если нет, то изменение перенаправления вывода может решить проблему переносимости. выгода здесь состоит в том, что никакие фоновые процессы не вовлечены.

Сорен
источник
Не достаточно портативный. У меня нет ни ncстарого Unix Boxen, ни многих встроенных Linux.
Жиль "ТАК - перестань быть злым"
0

Другой способ запустить конвейер в своей собственной группе процессов - запустить sh -c '....'в псевдотерминале, используя scriptкоманду (которая неявно применяет setsidфункцию).

#!/bin/sh
stty -echo -onlcr
# GNU script
foo=`script -q -c 'sh -c "{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }" 3>&1 2>/dev/null' /dev/null`
# FreeBSD script
#foo=`script -q /dev/null sh -c '{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }' 3>&1 2>/dev/null`
stty echo onlcr
echo "foo: $foo"


# alternative without: stty -echo -onlcr
# cr=`printf '\r'`
# foo=`script -q -c 'sh -c "{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null"' /dev/null | sed -e "s/${cr}$//" -ne 'p;N'`  # GNU
# foo=`script -q /dev/null sh -c '{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null' | sed -e "s/${cr}$//" -ne 'p;N'`  # FreeBSD
# echo "foo: $foo"
Пит
источник
Не достаточно портативный. У меня нет ни scriptстарого Unix Boxen, ни многих встроенных Linux.
Жиль "ТАК - перестань быть злым"
0

Ответ в https://unix.stackexchange.com/a/18711 очень хороший.

Я хотел достичь аналогичного результата, но без необходимости явного вызова оболочки снова, потому что я хотел вызвать существующие функции оболочки.

Используя bash, можно сделать следующее:

eval 'set -m ; ( ... ) ; '"$(set +o)"

Итак, предположим, у меня уже есть функция оболочки f:

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

Выполняя это я вижу:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
граф
источник
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
Кори Шварц
источник
Это не работает по той же причине, что и ответ geekausor . Мне нужно получить вывод внешней команды, и сигнал игнорируется во время выполнения этой команды.
Жиль "ТАК - перестань быть злым"