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 for
loop para um novo script loop.sh
, play.sh
tornando-se assim o processo pai e loop.sh
o 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 SIGINT
sinal, ele o propaga para todos os processos filhos, portanto meu segundo caso está falhando. Existe alguma maneira de evitar SIGINT
a propagação para processos filhos e assim fazer o loop.sh
trabalho 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.sh
e 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 wait
e, ao receber o sinal INT, kill
o grupo de processos cujo PGID é igual ao seu PID.
Para o script em questão,
play.sh
isso pode ser feito da seguinte maneira:Na
stop()
função substituaexit 1
porkill -TERM -$$ # note the dash, negative PID, kills the process group
Iniciar
loop.sh
como um processo em segundo plano (vários processos em segundo plano podem ser iniciados aqui e gerenciados porplay.sh
)loop.sh &
Adicione
wait
no 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.sh
iniciou três sleep
processos em segundo plano e agora está wait
neles. 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 kill
com o valor negativo de PGID
. Se você fornecer kill
um número negativo, ele será usado como -PGID. Como o PID do script ( $$
) é igual ao PGID, você pode kill
agrupar 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 trap
manipulador de sinal, gera 3 filhos e, em seguida, entra em um loop de espera sem fim, esperando para ser eliminado pelo kill
comando 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/sh
a 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 SIGINT
do 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.sh
fonte, 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+C
você mata um sleep
- não o seush
(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 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 ''
SIG
ignorado pelo pai.
Você também precisa -m
de monitoramento controlado por trabalho em seus pais para que ele acompanhe seus filhos. wait
, por exemplo, só funciona com controle de trabalho. -m
O 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]$