Como posso matar e aguardar que os processos em segundo plano sejam finalizados em um shell script quando pressiono Ctrl + C?

24

Estou tentando configurar um script de shell para que ele execute processos em segundo plano e, quando eu Ctrlco script de shell, mata os filhos e sai.

O melhor que eu consegui fazer é esse. Parece que o kill 0 -INTscript também mata o script antes que a espera aconteça; portanto, o script do shell morre antes que os filhos sejam concluídos.

Alguma idéia de como eu posso fazer esse script de shell esperar que as crianças morram após o envio INT?

#!/bin/bash
trap 'killall' INT

killall() {
    echo "**** Shutting down... ****"
    kill 0 -INT
    wait # Why doesn't this wait??
    echo DONE
}

process1 &
process2 &
process3 &

cat # wait forever
Slipheed
fonte
Encontrei essa pergunta pesquisando "shell intercept kill switch", obrigado por compartilhar o comando trap, é muito útil executar um comando após sair de um aplicativo com Ctrl + C quando o aplicativo não sai corretamente através do botão fechar GUI.
baptx 24/11

Respostas:

22

Seu killcomando está ao contrário.

Como muitos comandos UNIX, as opções que começam com menos devem vir primeiro, antes de outros argumentos.

Se você escrever

kill -INT 0

-INTcomo uma opção e envia SIGINTpara 0( 0é um número especial que significa todos os processos no grupo de processos atual).

Mas se você escrever

kill 0 -INT

ele vê o 0, decide que não há mais opções, então usa SIGTERMpor padrão. E envia isso para o grupo de processos atual, o mesmo que se você fizesse

kill -TERM 0 -INT    

(ele também iria tentar enviar SIGTERMpara -INT, o que causaria um erro de sintaxe, mas ele envia SIGTERMa 0primeira, e nunca fica tão longe.)

Portanto, seu script principal está recebendo um SIGTERMantes de executar o waite echo DONE.

Adicionar

trap 'echo got SIGTERM' TERM

no topo, logo após

trap 'killall' INT

e execute-o novamente para provar isso.

Como Stephane Chazelas aponta, seus filhos em segundo plano ( process1, etc.) ignoram SIGINTpor padrão.

De qualquer forma, acho que enviar SIGTERMfaria mais sentido.

Finalmente, não tenho certeza se kill -process groupé garantido ir primeiro às crianças. Ignorar sinais durante o desligamento pode ser uma boa ideia.

Então tente o seguinte:

#!/bin/bash
trap 'killall' INT

killall() {
    trap '' INT TERM     # ignore INT and TERM while shutting down
    echo "**** Shutting down... ****"     # added double quotes
    kill -TERM 0         # fixed order, send TERM not INT
    wait
    echo DONE
}

./process1 &
./process2 &
./process3 &

cat # wait forever
Mikel
fonte
Obrigado, isso funciona! Isso parece ter sido um problema.
SlipHeded #
9

Infelizmente, os comandos iniciados em segundo plano são definidos pelo shell para ignorar o SIGINT e, pior ainda, não podem ser ignorados trap. Caso contrário, tudo o que você precisa fazer é

(trap - INT; exec process1) &
(trap - INT; exec process2) &
trap '' INT
wait

Como process1 e process2 obteriam o SIGINT quando você pressionar Ctrl-C, pois eles fazem parte do mesmo grupo de processos, que é o grupo de processos em primeiro plano do terminal.

O código acima funcionará com pdksh e zsh que, a esse respeito, não são compatíveis com POSIX.

Com outros shells, você precisaria usar outra coisa para restaurar o manipulador padrão do SIGINT, como:

perl -e '$SIG{INT}=DEFAULT; exec "process1"' &

ou use um sinal diferente como SIGTERM.

Stéphane Chazelas
fonte
2

Para aqueles que querem apenas matar um processo e esperar que ele morra, mas não indefinidamente :

Aguarda no máximo 60 segundos por tipo de sinal.

Aviso: Esta resposta não tem nenhuma relação com capturar e enviar um sinal de interrupção.

# close_app_sub GREP_STATEMENT SIGNAL DURATION_SEC
# GREP_STATEMENT must not match itself!
close_app_sub() {
    APP_PID=$(ps -x | grep "$1" | grep -oP '^\s*\K[0-9]+' --color=never)
    if [ ! -z "$APP_PID" ]; then
        echo "App is open. Trying to close app (SIGNAL $2). Max $3sec."
        kill $2 "$APP_PID"
        WAIT_LOOP=0
        while ps -p "$APP_PID" > /dev/null 2>&1; do
            sleep 1
            WAIT_LOOP=$((WAIT_LOOP+1))
            if [ "$WAIT_LOOP" = "$3" ]; then
                break
            fi
        done
    fi
    APP_PID=$(ps -x | grep "$1" | grep -oP '^\s*\K[0-9]+' --color=never)
    if [ -z "$APP_PID" ]; then return 0; else return "$APP_PID"; fi
}

close_app() {
    close_app_sub "$1" "-HUP" "60"
    close_app_sub "$1" "-TERM" "60"
    close_app_sub "$1" "-SIGINT" "60"
    close_app_sub "$1" "-KILL" "60"
    return $?
}

close_app "[f]irefox"

Ele seleciona o aplicativo para matar por nome ou argumentos. Mantenha os colchetes da primeira letra do nome do aplicativo para evitar a correspondência com o grep.

Com algumas alterações, você pode usar diretamente o PID ou um método mais simples, em pidof process_namevez da psinstrução.

Detalhes do código: grep final é obter o PID sem os espaços finais.

KrisWebDev
fonte
1

Se o que você deseja é gerenciar alguns processos em segundo plano, por que não usar os recursos de controle de tarefas do bash?

$ gedit &
[1] 2581
$ emacs &
[2] 2594
$ jobs
[1]-  Running                 gedit &
[2]+  Running                 emacs &
$ jobs -p
2581
2594
$ kill -2 `jobs -p`
$ jobs
[1]-  Interrupt               gedit
[2]+  Done                    emacs
Máxima
fonte
0

Então, eu brinquei com isso também. No Bash, você pode definir funções e fazer coisas sofisticadas com elas. Meus colegas e eu usamos terminator, portanto, para execuções em lote, normalmente geramos um monte de janelas de terminador, se queremos visualizar a saída (menos elegante que as tmuxguias, mas você pode usar uma interface mais semelhante à GUI haha).

Aqui está a configuração que eu criei, bem como um exemplo de coisas que você pode executar:

#!/bin/bash
custom()
{
    terun 'something cool' 'yes'
    run 'xclock'
    terun 'echoes' 'echo Hello; echo Goodbye; read'
    func()
    {
        local i=0
        while :
        do
            echo "Iter $i"
            let i+=1
            sleep 0.25
        done
    }
    export -f func
    terun 'custom func' 'func'
}

# [ Setup ]
run()
{
    "$@" &
    # Give process some time to start up so windows are sequential
    sleep 0.05
}
terun()
{
    run terminator -T "$1" -e "$2"
}
finish()
{
    procs="$(jobs -p)"
    echo "Kill: $procs"
    # Ignore process that are already dead
    kill $procs 2> /dev/null
}
trap 'finish' 2

custom

echo 'Press <Ctrl+C> to kill...'
wait

Executando

$ ./program_spawner.sh 
Press <Ctrl+C> to kill...
^CKill:  9835
9840
9844
9850
$ 

Edite @ Maxim, acabei de ver sua sugestão, que a torna muito mais simples! Obrigado!

eacousineau
fonte