Как обработать прерывание SIGINT с помощью приглашения пользователя в скрипте оболочки?

Как обработать прерывание SIGINT с помощью приглашения пользователя в скрипте оболочки?

Я пытаюсь обработать SIGINT( CtrlC) таким образом, что если пользователь случайно нажмет ctrl-c, ему будет выдано сообщение «Вы хотите выйти? (y/n)». Если он введет «да», то выйдите из скрипта. Если «нет», то продолжите с того места, где произошло прерывание. По сути, мне нужно CtrlCработать аналогично SIGTSTP( CtrlZ), но немного по-другому. Я пробовал разные способы добиться этого, но не получил ожидаемых результатов. Ниже приведены несколько сценариев, которые я пробовал.

Дело 1

Сценарий:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
} 
trap 'stop' SIGINT 
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

При запуске приведенного выше скрипта я получаю ожидаемые результаты.

Выход:

$ play.sh 
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!! 
$  

Случай: 2

Я перенес forцикл в новый скрипт loop.sh, таким образом, play.shон становится родительским процессом и loop.shдочерним процессом.

Сценарий:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
}
trap 'stop' SIGINT 
loop.sh

Сценарий:loop.sh

#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

В этом случае результат не соответствует ожидаемому.

Выход:

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$

Я понимаю, что когда процесс получает SIGINTсигнал, он распространяет его на все дочерние процессы, поэтому мой 2-й случай терпит неудачу. Есть ли способ избежать SIGINTраспространения на дочерние процессы и таким образом сделать так, чтобы loop.shработа выполнялась точно так же, как в 1-м случае?

Примечание: Это всего лишь пример моего реального приложения. Приложение, над которым я работаю, имеет несколько дочерних скриптов в play.shи loop.sh. Я должен убедиться, что приложение при получении SIGINTне должно завершаться, а должно выводить пользователю сообщение.

решение1

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

Для этого после запуска дочерних процессов (loop.sh) в фоновом режиме вызовите wait, а при получении сигнала INT — killгруппу процессов, PGID которой равен вашему PID.

  1. Для рассматриваемого сценария play.shэто можно сделать следующим образом:

  2. В stop()функции замените exit 1на

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
  3. Запустить loop.shкак фоновый процесс (здесь можно запустить несколько фоновых процессов и управлять ими play.sh)

    loop.sh &
    
  4. Добавьте waitв конец скрипта ожидание всех детей.

    wait
    

Когда ваш скрипт запускает процесс, этот дочерний процесс становится членом группы процессов с PGID, равным PID родительского процесса, находящегося $$в родительской оболочке.

Например, скрипт trap.shзапустил три sleepпроцесса в фоновом режиме и теперь waitвыполняет их. Обратите внимание, что столбец идентификатора группы процессов (PGID) совпадает с PID родительского процесса:

  PID  PGID STAT COMMAND
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600

В Unix и Linux вы можете отправить сигнал каждому процессу в этой группе процессов, вызвав killс отрицательным значением PGID. Если вы дадите killотрицательное число, оно будет использоваться как -PGID. Поскольку PID скрипта ( $$) совпадает с его PGID, вы можете указать killсвою группу процессов в оболочке с помощью

kill -TERM -$$    # note the dash before $$

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

Простой код ниже иллюстрирует все это. Он устанавливает trapобработчик сигнала, порождает 3 потомка, затем переходит в бесконечный цикл ожидания, ожидая, чтобы убить себя командой killгруппы процессов в обработчике сигнала.

$ cat trap.sh
#!/bin/sh

signal_handler() {
        echo
        read -p 'Interrupt: ignore? (y/n) [Y] >' answer
        case $answer in
                [nN]) 
                        kill -TERM -$$  # negative PID, kill process group
                        ;;
        esac
}

trap signal_handler INT 

for i in 1 2 3
do
    sleep 600 &
done

wait  # don't exit until process group is killed or all children die

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

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17111 17111 R+   ps -o pid,pgid,stat,args
$ 

OK, никаких дополнительных процессов не запущено. Запустите тестовый скрипт, прервите его ( ^C), выберите игнорирование прерывания, а затем приостановите его ( ^Z):

$ sh trap.sh 
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+  Stopped                 sh trap.sh
$

Проверьте запущенные процессы, обратите внимание на номера групп процессов ( PGID):

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600
17143 17143 R+   ps -o pid,pgid,stat,args
$

Выведем наш тестовый скрипт на передний план ( fg) и снова прервем ( ^C), на этот раз выбравнетигнорировать:

$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$

Проверьте запущенные процессы, больше никакого сна:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17159 17159 R+   ps -o pid,pgid,stat,args
$ 

Примечание о вашей оболочке:

Мне пришлось изменить ваш код, чтобы он запустился на моей системе. У вас есть #!/bin/shпервая строка в ваших скриптах, но скрипты используют расширения (из bash или zsh), которые недоступны в /bin/sh.

решение2

«Я понимаю, что когда процесс получает сигнал SIGINT, он распространяет сигнал на все дочерние процессы»

Откуда у вас эта неверная идея?

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$

Если бы имело место распространение, как утверждается, можно было бы ожидать «ой» от дочернего процесса.

В действительности:

«Всякий раз, когда мы нажимаем клавишу прерывания на нашем терминале (часто DELETE или Control-C) или клавишу выхода (часто Control-обратная косая черта), это приводит к отправке либо сигнала прерывания, либо сигнала выхода всем процессам в группе процессов переднего плана» — У. Ричард Стивенс. «Расширенное программирование в среде UNIX®». Addison-Wesley. 1993. стр. 246.

Это можно наблюдать через:

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$

Поскольку все процессы группы процессов переднего плана будут получать сигнал SIGINTот Control+C, вам необходимо спроектировать все процессы так, чтобы они правильно обрабатывали или игнорировали этот сигнал, в зависимости от ситуации, или, возможно, сделать так, чтобы подпроцессы стали новой группой процессов переднего плана, чтобы родительский процесс (например, оболочка) не видел сигнал, поскольку он больше не находится в группе процессов переднего плана.

решение3

В play.shисходном файле цикла такой:

source ./loop.sh

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

решение4

Проблема в sleep.

Когда тыCTRL+Cты убиваешь sleep- не твойsh (даже если синтаксис в вашем случае #!/bin/shнемного подозрителен). Однако вся группа процессов получает сигнал, поскольку вы не устанавливаете свой обработчик в loop.sh(который является подоболочкой) — он сразу же завершается.

Вам нужна ловушка в loop.sh. Ловушки очищаются для каждой запущенной подоболочки, если они явно не указаныtrap ''SIGигнорируется родителем.

Вам также необходимо контролировать -mзадания в родительском процессе, чтобы он отслеживал своих дочерних процессов. waitНапример, работает только при управлении заданиями. -mРежим контроля — это то, как оболочки взаимодействуют с терминалами.

sh  -cm '  . /dev/fd/3' 3<<""                     # `-m`onitor is important
     sh -c ' kill -INT "$$"'&                     # get the sig#
     wait %%;INT=$?                               # keep it in $INT
     trap  " stty $(stty -g;stty -icanon)" EXIT   # lose canonical input
     loop()( trap    exit INT                     # this is a child
             while   [ "$#" -le 100 ]             # same as yours
             do      echo "$#"                    # writing the iterator
                     set "" "$@"                  # iterating
                     sleep 3                      # sleeping
             done
     )
     int(){  case    $1      in                   # handler fn
             ($INT)  printf  "\33[2K\rDo you want to stop playing? "
                     case    $(dd bs=1 count=1; echo >&3)  in
                     ([Nn])  return
             esac;   esac;   exit "$1"
     }       3>&2    >&2     2>/dev/null
     while   loop ||
             int "$?"
     do :;   done

0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$

Связанный контент