Tenho um programa que ignora o SIGINT, mas que quero executar em primeiro plano. Eu gostaria de encontrar uma maneira de forçá-lo a fechar com Ctrl-C. Existe alguma maneira de escrever um wrapper (que você chamaria ./wrapper.sh my_program
) que forçaria o encerramento do programa com comportamento incorreto, potencialmente detectando o SIGINT ignorado e gerando um SIGKILL?
Esta respostaé exatamente o oposto do que estou procurando - gostaria de forçar um programa que ignora um sinal para encerrar no SIGINT.
Responder1
Vamos construir um wrapper que "converte" SIGINT em SIGKILL.
Precisamos de um processo que seja executado my_program
e obtenha SIGINT em Ctrl+ c. Ao receber SIGINT, ele deverá enviar SIGKILL para my_program
. Observe que não precisamos my_program
receber SIGINT causado por Ctrl+ c; basta que seja SIGKILL causado pelo processo extra.
Fatos relevantes:
- Uma maneira padrão de executar um processo junto com outro é executar um deles de forma assíncrona em segundo plano (ou seja, com terminação
&
). - Quando você pressiona Ctrl+ c, é o seu emulador de terminal quem envia SIGINT para processos no grupo de processos em primeiro plano.
- Alguns shells (simples) executam tudo no mesmo grupo de processos. Se eles forem instruídos a executar um comando em segundo plano, eles redirecionarão seu stdin para
/dev/null
ou arquivo equivalente para evitar que o comando roube entrada. - Outros shells podem executar cada comando em um grupo de processos separado. Se eles forem instruídos a executar um comando em segundo plano, eles deixarão seu stdin como está; ainda assim, o comando em segundo plano não será capaz de roubar a entrada do terminal de controle por causa de
SIGTTIN
. Isso permite que o shell mova um trabalho do segundo plano para o primeiro plano, informando ao terminal o novo grupo de processos em primeiro plano. O mecanismo é chamado de controle de trabalho e pode ser desabilitado. Nos scripts está desabilitado por padrão. - De qualquer forma, um processo em segundo plano não pode ler no terminal. Isso significa que não devemos executar
my_program
em segundo plano caso o stdin seja o terminal emy_program
precise lê-lo. - Infelizmente, em alguns shells também não devemos executar o outro processo em segundo plano. Alguns shells usam grupos de processos separados mesmo quando o controle de tarefas está desabilitado. O outro processo que não estiver no grupo de processos em primeiro plano não receberá SIGINT em Ctrl+ c, portanto não poderá "convertê-lo" em SIGKILL.
- No meu Debian 10
posh
existe um shell que executa tudo no mesmo grupo de processos.
Isso leva ao seguinte wrapper:
#!/usr/bin/env posh
( trap 'kill -s KILL 0' INT
while kill -s 0 "$$" 2>/dev/null; do sleep 1; done
) &
exec "$@"
exec "$@"
é executado my_program
(ou o que você especificar), possivelmente com argumentos. Graças a exec
my_program
substituirá o invólucro. Não só será capaz de ler o stdin que pode ser um terminal; seu PID será o do wrapper substituído. Nesse sentido, o invólucro é transparente. Além disso, isso tem um recurso interessante: o PID de my_program
é conhecido antes my_program
do início e podemos usá-lo facilmente em outro processo (neste caso, em um subshell em segundo plano) para detectar quando my_program
realmente termina.
O trap
"converte" SIGINT em SIGKILL. Nota kill -s KILL 0
envia SIGKILL para todo o grupo de processos, incluindo filhos de my_program
se eles estiverem no grupo (se você quiser matar my_program
apenas, use kill -s KILL "$$"
). No entanto kill -s 0 "$$"
testa a existência de my_program
apenas.
Existe uma alternativa que não requer um shell que execute tudo no mesmo grupo de processos. O truque é: os processos em um pipeline devem ser executados em um grupo de processos; e com um redirecionamento inteligente você pode construir um pipeline onde as peças não estão conectadas umas às outras.
#!/bin/sh -
exec 9>&1
( "$@"; kill -s TERM 0 ) >&9 9>&- | (
trap 'kill -s KILL 0' INT
while :; do sleep 1; done
)
Nesta variante my_program
não substituirá o invólucro. A segunda kill
é "converter" SIGINT em SIGKILL. A primeira kill
é encerrar o loop caso my_program
saia em circunstâncias em que o loop sobreviveria.
Existem alguns cenários que podem não funcionar conforme o esperado (com qualquer wrapper). Entre eles:
- Se
my_program
bifurca e sai, deixando o verdadeiro trabalho para a criança. O programa problemático em questão provavelmente não se comporta assim, já que você tentou Ctrl+ c-lo. Mas, em geral, pode. - Se
my_program
gerar processos filhos em outro(s) grupo(s) de processos e você quiser eliminá-los junto com seus pais. - Se
my_program
configura o terminal para não enviar SIGINT em Ctrl+ c. Se você suspeitar que isso acontece, melhore o wrapper colocandostty -F /dev/tty intr ^C
entresleep 1
edone
. Neste caso o programa pode até não ignorar o SIGINT, ele pode simplesmente garantir que não o obterá do terminal. Então talvez SIGKILL seja um exagero; talvez seja o suficiente para restaurar esta funcionalidade do terminal e Ctrl+ ccomeçará a funcionar.
De um comentário:
Os programas que ignoram o SIGINT geralmente fazem isso por um bom motivo.
Verdadeiro. Experimente SIGTERM ou SIGHUP em vez de SIGKILL. Talvez o programa em questão não ignore pelo menos um deles e saia normalmente com a limpeza adequada.