Como posso lidar com o SIGINT trap com um prompt de usuário no shell script?

6

Eu estou tentando lidar com SIGINT / CTRL + C interromper de tal forma que se um usuário acidentalmente pressiona ctrl-c, ele é solicitado com uma mensagem: "Deseja sair? (y / n)". Se ele digitar sim, saia do script. Se não, continue de onde a interrupção ocorreu. Basicamente, eu preciso que o Ctrl-C funcione de forma semelhante ao Ctrl-Z / SINTSTP, mas de uma maneira um pouco diferente. Eu tentei várias maneiras de conseguir isso, mas não obtive os resultados esperados. Abaixo estão alguns cenários que eu tentei.

Caso: 1

Script: 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"

Quando executo o script acima, obtenho os resultados esperados.

Saída:

$ 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 !!! 
$  

Caso: 2 Eu movi o loop for para um novo script loop.sh, assim play.sh se torna o processo pai e loop.sh o processo filho.

Script: 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

Script: loop.sh

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

A saída, neste caso, não é a esperada.

Saída:

$ 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
$

Eu entendo que quando um processo recebe um sinal SIGINT, ele propaga o sinal para todos os processos filhos, assim meu segundo caso está falhando. Existe alguma maneira que eu possa evitar SIGINT sendo propagado para processos filho e, assim, fazer o loop.sh funcionar exatamente da maneira que funcionou no primeiro caso?

Nota: Este é apenas um exemplo da minha aplicação real. O aplicativo em que estou trabalhando possui vários scripts filhos em play.sh e loop.sh. Devo ter certeza de que o aplicativo ao receber o SIGINT, não deve terminar, mas deve solicitar ao usuário uma mensagem.

    
por Venkat Madhav 04.11.2015 / 00:50

4 respostas

4

Grande pergunta clássica sobre gerenciamento de trabalhos e sinais com bons exemplos! Desenvolvi um roteiro de teste simplificado para me concentrar na mecânica do tratamento de sinal.

Para fazer isso, depois de iniciar os filhos (loop.sh) em segundo plano, chame wait e, após o recebimento do sinal INT, kill do grupo de processos cujo PGID seja igual ao seu PID .

  1. Para o script em questão, play.sh , isso pode ser feito da seguinte forma:

  2. Na função stop() , substitua exit 1 por

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
  3. Inicie loop.sh como um processo em segundo plano (vários processos em segundo plano podem ser iniciados aqui e gerenciados por play.sh )

    loop.sh &
    
  4. Adicione wait no final do script para esperar por todas as crianças.

    wait
    

Quando o script inicia um processo, esse filho se torna um membro de um grupo de processos com PGID igual ao PID do processo pai, que é $$ no shell pai.

Por exemplo, o script trap.sh iniciou três sleep processos em segundo plano e agora está wait neles, observe que a coluna ID do grupo de processos (PGID) é a mesma do PID do processo pai:

  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

No Unix e no Linux, você pode enviar um sinal para cada processo nesse grupo de processos chamando kill com o valor negativo de PGID . Se você der a kill um número negativo, ele será usado como -PGID. Como o PID do script ( $$ ) é o mesmo que o PGID, você pode kill do seu grupo de processos no shell com

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

você precisa fornecer um número de sinal ou nome, caso contrário, algumas implementações de kill dirão "Opção ilegal" ou "especificação de sinal inválida".

O código simples abaixo ilustra tudo isso. Ele define um manipulador de sinal trap , gera 3 filhos e, em seguida, entra em um ciclo de espera sem fim, aguardando para se matar pelo comando kill process group no manipulador de sinal.

$ 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

Aqui está uma amostra de execução:

$ 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, não há processos extras em execução. Inicie o script de teste, interrompa-o ( ^C ), escolha ignorar a interrupção e, em seguida, suspenda-a ( ^Z ):

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

Verifique os processos em execução, observe os números do grupo de processos ( 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
$

Traga nosso script de teste para o primeiro plano ( fg ) e interrompa ( ^C ) novamente, desta vez escolha não para ignorar:

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

Verifique os processos em execução, sem mais sono:

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

Nota sobre o seu shell:

Eu tive que modificar seu código para que ele fosse executado no meu sistema. Você tem #!/bin/sh como a primeira linha em seus scripts, mas os scripts usam extensões (do bash ou zsh) que não estão disponíveis em / bin / sh.

    
por 04.11.2015 / 02:15
1

Em play.sh , forneça o arquivo de loop assim:

source ./loop.sh

Uma subcamada que saiu uma vez devido a uma armadilha não pode ser devolvida ao sair da armadilha.

    
por 04.11.2015 / 01:14
1

"Eu entendo que quando um processo recebe um sinal SIGINT, ele propaga o sinal para todos os processos filhos"

De onde você tirou essa ideia incorreta?

$ 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
$

Se houve propagação, como alegado, pode-se esperar um "ai" do processo filho.

Na realidade:

"Whenever we type our terminal's interrupt key (often DELETE or Control-C) or quit key (often Control-backslash) this causes either the interrupt signal or the quit signal to be sent to all processes in the foreground process group" -- W. Richard Stevens. "Advanced Programming in the UNIX® Environment". Addison-Wesley. 1993. p.246.

Isso pode ser observado via:

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

Como todos os processos do grupo de processos em primeiro plano consumirão SIGINT do Control + C , será necessário projetar todos os processos para manipular ou ignorar adequadamente esse sinal, conforme apropriado, ou Possivelmente, os subprocessos se tornam o novo grupo de processos em primeiro plano, de modo que o pai (por exemplo, o shell) não vê o sinal, pois não está mais no grupo de processos em primeiro plano.

    
por 04.11.2015 / 01:58
0

O problema é sleep .

Quando você CTRL+C mata um sleep - não o seu sh (mesmo que a sintaxe no seu #!/bin/sh seja um pouco suspeita) . Todo o grupo de processos recebe o sinal, porque você não instala seu manipulador em loop.sh - que é um subshell - que termina imediatamente depois.

Você precisa de uma armadilha em loop.sh . As interceptações são limpas para cada subshell iniciado, a menos que elas sejam explicitamente trap '' SIG ignoradas pelo pai.

Você também precisa de um -m onitoring controlado pelo trabalho em seus pais para que ele acompanhe seus filhos. wait , por exemplo, funciona apenas com controle de tarefas. -m onitor mode é como os shells interagem com os terminais.

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  "[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]$
    
por 04.11.2015 / 07:10