Почему я не могу уничтожить тайм-аут, названный из сценария Bash с нажатием клавиши?

TL, DR:

array_of_lines=("${(@f)$(my_command)}")

Первая ошибка (→ Q2): IFS='\n' наборы IFS к этим двум символам \ и n. Установить IFS к новой строке использовать IFS=$'\n'.

Вторая ошибка: для установки переменной на значение массива Вам нужны круглые скобки вокруг элементов: array_of_lines=(foo bar).

Это работало бы, за исключением того, что это разделяет пустые строки, потому что последовательный пробел рассчитывает как единственный разделитель:

IFS=$'\n' array_of_lines=($(my_command))

Можно сохранить пустые строки кроме в самом конце путем удвоения пробельного символа в IFS:

IFS=$'\n\n' array_of_lines=($(my_command))

Чтобы продолжать запаздывать пустые строки также, необходимо было бы добавить что-то к выводу команды, потому что это происходит в самой замене команды, не от парсинга его.

IFS=$'\n\n' array_of_lines=($(my_command; echo .)); unset 'array_of_lines[-1]'

(принятие вывода my_command не заканчивается в неразграниченной строке; также обратите внимание потерю статуса выхода my_command)

Обратите внимание что все отрывки выше отпуска IFS с его значением не по умолчанию, таким образом, они могут испортить последующий код. Сохранять установку IFS локальный, помещает все это в функцию, где Вы объявляете IFS локальный (здесь также заботящийся о сохранении статуса выхода команды):

collect_lines() {
  local IFS=$'\n\n' ret
  array_of_lines=($("$@"; ret=$?; echo .; exit $ret))
  ret=$?
  unset 'array_of_lines[-1]'
  return $ret
}
collect_lines my_command

Но я рекомендую не смешать с IFS; вместо этого, используйте f флаг расширения для разделения на новых строках (→ Q1):

array_of_lines=("${(@f)$(my_command)}")

Или сохранить запаздывание пустых строк:

array_of_lines=("${(@f)$(my_command; echo .)}")
unset 'array_of_lines[-1]'

Значение IFS не имеет значения там. Я подозреваю, что Вы использовали команду, которая разделяет на IFS распечатать $array_of_lines в Ваших тестах (→ Q3).

11
23.05.2017, 15:40
4 ответа

Ключи сигнала, такие как Ctrl+C отправляют сигнал во все процессы в группе приоритетного процесса.

В типичном случае группа процесса является конвейером. Например, в head <somefile | sort, выполнение процесса head и выполнение процесса sort находятся в той же группе процесса, как оболочка, таким образом, они все получают сигнал. Когда Вы выполняете задание в фоновом режиме (somecommand &), то задание находится в своей собственной группе процесса, так нажатие Ctrl+C не влияет на него.

timeout программа ставит себя в свою собственную группу процесса. От исходного кода:

/* Ensure we're in our own group so all subprocesses can be killed.
   Note we don't just put the child in a separate group as
   then we would need to worry about foreground and background groups
   and propagating signals between them.  */
setpgid (0, 0);

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

Когда Вы работаете timeout непосредственно на командной строке и нажимают Ctrl+C, получающийся SIGINT получен оба timeout и дочерним процессом, но не интерактивной оболочкой, которая является timeoutродительский процесс. Когда timeout назван из сценария, только оболочка, запускающая скрипт, получает сигнал: timeout не получает его, так как это находится в другой группе процесса.

Можно установить обработчик сигналов в сценарии оболочки с trap встроенный. К сожалению, дело не в этом простой. Рассмотрите это:

#!/bin/sh
trap 'echo Interrupted at $(date)' INT
date
timeout 5 sleep 10
date

При нажатии Ctrl+C после 2 секунд это все еще ожидает целые 5 секунд, то распечатайте “Прерванное” сообщение. Поэтому оболочка воздерживается от выполнения кода прерывания, в то время как задание переднего плана активно.

Для исправления этого выполните задание в фоновом режиме. В обработчике сигналов звонить kill передавать сигнал к timeout группа процесса.

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid
18
27.01.2020, 19:57
  • 1
    работала красиво! Я намного более умен теперь, мерси! –  meetar 06.12.2012, 19:12

В основном Ctrl+C отправляет a SIGINT сигнал, в то время как Ctrl+Z отправляют a SIGTSTP сигнал.

SIGTSTP просто останавливает процесс, a SIGCONT продолжит его.

Это работает над приоритетным процессом, который был разветвлен на командной строке.

Если Ваш процесс является фоновым процессом, необходимо будет отправить тот сигнал в процессы по-другому. kill сделает это. В теории a "-" - оператор, на котором уничтожение должно также сигнализировать о дочерних процессах, но это редко работает как ожидалось.

Для дополнительных материалов для чтения: сигналы Unix

1
27.01.2020, 19:57
  • 1
    Это верно, по крайней мере, до “редко работ как ожидалось” (который зависит от Ваших ожиданий — необходимо читать о группах процесса), но абсолютно не важный. Вопрос не предает беспорядка о SIGINT и SIGTSTP. –  Gilles 'SO- stop being evil' 06.12.2012, 03:02

Основываясь на отличном ответе Жиля. У команды timeout есть опция foreground, если она используется, то нажатие CTRL+C приведет к выходу из команды timeout.

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
13
27.01.2020, 19:57

Улучшение ответа @Gilles путем предоставления подхода «найти и заменить» для существующих скриптов:

  1. Добавьте этот фрагмент в начало кода:
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see https://superuser.com/a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}
  1. Добавьте (или объедините )следующий код в ваш обработчик INT:
pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see https://superuser.com/a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            #echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
  1. Замените команды timeoutна функцию my_timeoutв вашем скрипте.

Пример

Вот пример скрипта:

#!/bin/bash

# see "killing timeout": https://unix.stackexchange.com/a/57692/65781
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see https://superuser.com/a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}

cleanup(){
    echo "-----------------------------------------"
    echo "Restoring previous routing table settings"
}

pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see https://superuser.com/a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
trap cleanup EXIT

echo "Executing 5 temporary timeouts..."
unreachable_ip="192.168.44.5"
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
echo "ctrl+c now to execute cleanup"
my_timeout 9s ping -c 1 "$unreachable_ip" &> /dev/null 
0
27.01.2020, 19:57

Теги

Похожие вопросы