Como posso lidar com a armadilha SIGINT com um prompt do usuário em shell script?

Como posso lidar com a armadilha SIGINT com um prompt do usuário em shell script?

Estou tentando lidar com SIGINT( CtrlC) de forma que, se um usuário pressionar acidentalmente ctrl-c, ele receba uma mensagem: "Deseja sair? (s/n)". Se ele digitar sim, saia do script. Se não, continue de onde ocorreu a interrupção. Basicamente, preciso CtrlCtrabalhar de forma semelhante a SIGTSTP( CtrlZ), mas de uma maneira um pouco diferente. 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

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

Mudei o forloop para um novo script loop.sh, play.shtornando-se assim o processo pai e loop.sho processo filho.

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

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

Entendo que quando um processo recebe um SIGINTsinal, ele o propaga para todos os processos filhos, portanto meu segundo caso está falhando. Existe alguma maneira de evitar SIGINTa propagação para processos filhos e assim fazer o loop.shtrabalho exatamente como funcionou no 1º caso?

Observação: Este é apenas um exemplo da minha aplicação real. O aplicativo no qual estou trabalhando possui vários scripts filhos em play.she loop.sh. Devo ter certeza de que o aplicativo, ao receber SIGINT, não deve ser encerrado, mas deve avisar o usuário com uma mensagem.

Responder1

Ótima pergunta clássica sobre gerenciamento de empregos e sinais com bons exemplos! Desenvolvi um script de teste simplificado para focar na mecânica do tratamento do sinal.

Para isso, após iniciar os filhos (loop.sh) em background, chame waite, ao receber o sinal INT, killo grupo de processos cujo PGID é igual ao seu PID.

  1. Para o script em questão, play.shisso pode ser feito da seguinte maneira:

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

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

    loop.sh &
    
  4. Adicione waitno final do script para esperar por todos os filhos.

    wait
    

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

Por exemplo, o script trap.shiniciou três sleepprocessos em segundo plano e agora está waitneles. Observe que a coluna de ID do grupo de processos (PGID) é igual ao 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 killcom o valor negativo de PGID. Se você fornecer killum número negativo, ele será usado como -PGID. Como o PID do script ( $$) é igual ao PGID, você pode killagrupar seu processo no shell com

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

você deve fornecer um número ou nome de sinal, 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 trapmanipulador de sinal, gera 3 filhos e, em seguida, entra em um loop de espera sem fim, esperando para ser eliminado pelo killcomando do grupo de processos 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á um exemplo 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, nenhum processo extra em execução. Inicie o script de teste, interrompa-o ( ^C), opte por ignorar a interrupção e depois 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 dos grupos 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 primeiro plano ( fg) e interrompa ( ^C) novamente, desta vez escolhanãoignorar:

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

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

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

Tive que modificar seu código para que ele funcionasse em meu sistema. Você tem #!/bin/sha primeira linha em seus scripts, mas os scripts usam extensões (de bash ou zsh) que não estão disponíveis em/bin/sh.

Responder2

“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 houvesse propagação, como afirmado, seria de se esperar um "ai" do processo filho.

Na realidade:

"Sempre que digitamos a tecla de interrupção do nosso terminal (geralmente DELETE ou Control-C) ou a tecla quit (geralmente Control-barra invertida), isso faz com que o sinal de interrupção ou o sinal de encerramento seja enviado para todos os processos no grupo de processos em primeiro plano" - W .Ricardo Stevens. “Programação Avançada no Ambiente UNIX®”. Addison-Wesley. 1993. p.246.

Isso pode ser observado através de:

$ 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 um SIGINTdo Control+C, você precisará projetar todos os processos para manipular ou ignorar adequadamente esse sinal, conforme apropriado, ou possivelmente fazer com que os subprocessos se tornem o novo grupo de processos em primeiro plano para que o pai ( por exemplo, o shell) não vê o sinal, pois ele não está mais no grupo de processos em primeiro plano.

Responder3

Na play.shfonte, o arquivo de loop assim:

source ./loop.sh

Um subshell, uma vez encerrado devido a uma armadilha, não pode ser retornado ao sair da armadilha.

Responder4

O problema é sleep.

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

Você precisa de uma armadilha loop.sh. As armadilhas são limpas para cada subshell iniciado, a menos que sejam explicitamentetrap ''SIGignorado pelo pai.

Você também precisa -mde monitoramento controlado por trabalho em seus pais para que ele acompanhe seus filhos. wait, por exemplo, só funciona com controle de trabalho. -mO modo onitor é 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  "\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]$

informação relacionada