¿Cómo puedo matar y esperar a que finalicen los procesos en segundo plano en un script de shell cuando lo presiono Ctrl + C?

24

Estoy tratando de configurar un script de shell para que ejecute procesos en segundo plano, y cuando uso Ctrlcel script de shell, mata a los hijos y luego sale.

Lo mejor que he logrado hacer es esto. Parece que kill 0 -INTtambién mata el script antes de que ocurra la espera, por lo que el script de shell muere antes de que los niños completen.

¿Alguna idea sobre cómo puedo hacer que este script de shell espere a que los niños mueran después de enviar 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
fuente
Encontré esta pregunta buscando "interruptor de interrupción de intercepción de shell", gracias por compartir el comando trap, es muy útil ejecutar un comando después de salir de una aplicación con Ctrl + C cuando la aplicación no sale correctamente a través del botón GUI cerrado.
Baptx

Respuestas:

22

Tu killcomando está al revés.

Al igual que muchos comandos de UNIX, las opciones que comienzan con un signo menos deben aparecer primero, antes que otros argumentos.

Si tú escribes

kill -INT 0

ve el -INTcomo una opción y envía SIGINTa 0( 0es un número especial que significa todos los procesos en el grupo de procesos actual).

Pero si escribes

kill 0 -INT

ve el 0, decide que no hay más opciones, por lo que usa SIGTERMde forma predeterminada. Y envía que al grupo de proceso actual, lo mismo que si lo hizo

kill -TERM 0 -INT    

(que también intente enviar SIGTERMa -INT, lo que provocaría un error de sintaxis, pero envía SIGTERMa 0la primera, y nunca llega tan lejos.)

Por lo tanto, su script principal está obteniendo un SIGTERMantes de ejecutar el waity echo DONE.

Añadir

trap 'echo got SIGTERM' TERM

en la parte superior, justo después

trap 'killall' INT

y ejecutarlo nuevamente para probar esto.

Como señala Stephane Chazelas, sus hijos en segundo plano ( process1, etc.) ignorarán SIGINTde forma predeterminada.

En cualquier caso, creo que enviar SIGTERMtendría más sentido.

Finalmente, no estoy seguro de si kill -process groupse garantiza ir primero a los niños. Ignorar las señales mientras se apaga puede ser una buena idea.

Así que prueba esto:

#!/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
fuente
Gracias, esto funciona! Eso parece haber sido un problema.
slipheed
9

Desafortunadamente, los comandos iniciados en segundo plano son configurados por el shell para ignorar SIGINT, y peor aún, no pueden ignorarlo trap. De lo contrario, todo lo que tendría que hacer es

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

Porque process1 y process2 obtendrían el SIGINT cuando presiona Ctrl-C ya que son parte del mismo grupo de procesos que es el grupo de procesos en primer plano del terminal.

El código anterior funcionará con pdksh y zsh que, en ese sentido, no son compatibles con POSIX.

Con otros shells, tendría que usar algo más para restaurar el controlador predeterminado para SIGINT como:

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

o use una señal diferente como SIGTERM.

Stéphane Chazelas
fuente
2

Para aquellos que solo quieren matar un proceso y esperar a que muera, pero no indefinidamente :

Espera un máximo de 60 segundos por tipo de señal.

Advertencia: esta respuesta no está relacionada de ninguna manera con atrapar una señal de muerte y enviarla.

# 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"

Selecciona la aplicación para matar por nombre o argumentos. Mantenga los corchetes para la primera letra del nombre de la aplicación para evitar coincidir con grep.

Con algunos cambios, puede usar directamente el PID o un simple en pidof process_namelugar de la psdeclaración.

Detalles del código: grep final es obtener el PID sin los espacios finales.

KrisWebDev
fuente
1

Si lo que desea es administrar algunos procesos en segundo plano, ¿por qué no utilizar las funciones de control de trabajos 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
fuente
0

Así que también jugué con esto. En Bash, puedes definir funciones y hacer cosas elegantes con ellas. Mis compañeros de trabajo y yo usamos terminator, por lo que para las ejecuciones por lotes normalmente generamos un montón de ventanas de terminación si queremos ver la salida (menos elegante que las tmuxpestañas, pero puede usar una interfaz más similar a la GUI jaja).

Aquí está la configuración que se me ocurrió, así como un ejemplo de cosas que puede ejecutar:

#!/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

Corriendo

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

Edite @Maxim, acabo de ver su sugerencia, ¡eso lo hace mucho más simple! ¡Gracias!

eacousineau
fuente