Come posso uccidere e aspettare che i processi in background finiscano in uno script shell quando lo premo Ctrl + C?


24

Sto cercando di impostare uno script shell in modo che esegua processi in background e quando io Ctrlclo script shell, uccide i bambini, quindi esce.

Il meglio che sono riuscito a inventare è questo. Sembra che kill 0 -INTuccida anche lo script prima che avvenga l'attesa, quindi lo script della shell muore prima del completamento dei bambini.

Qualche idea su come posso fare in modo che questo script di shell aspetti che i bambini muoiano dopo l'invio 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


Ho trovato questa domanda cercando "switch intercettazione kill shell", grazie per aver condiviso il comando trap, è molto utile eseguire un comando dopo essere usciti da un'app con Ctrl + C quando l'app non si chiude correttamente attraverso il pulsante di chiusura della GUI.
battezzato il

Risposte:


22

Il tuo killcomando è al contrario.

Come molti comandi UNIX, le opzioni che iniziano con un segno meno devono venire prima, prima di altri argomenti.

Se scrivi

kill -INT 0

vede l' -INTopzione come e invia SIGINTa 0( 0è un numero speciale che significa tutti i processi nel gruppo di processi corrente).

Ma se scrivi

kill 0 -INT

vede il 0, decide che non ci sono più opzioni, quindi utilizza SIGTERMper impostazione predefinita. E manda che al gruppo processo corrente, lo stesso come se avete fatto

kill -TERM 0 -INT    

(proverebbe anche a inviarlo SIGTERMa -INT, il che provocherebbe un errore di sintassi, ma invia SIGTERMper 0primo e non arriva mai così lontano.)

Quindi il tuo script principale sta ottenendo un SIGTERMprima di poter eseguire il waite echo DONE.

Inserisci

trap 'echo got SIGTERM' TERM

in alto, subito dopo

trap 'killall' INT

ed eseguirlo di nuovo per dimostrarlo.

Come sottolinea Stephane Chazelas, i tuoi figli in background ( process1, ecc.) Ignoreranno SIGINTper impostazione predefinita.

In ogni caso, penso che l'invio SIGTERMavrebbe più senso.

Infine, non sono sicuro che kill -process groupsia garantito per prima cosa andare dai bambini. Ignorare i segnali durante lo spegnimento potrebbe essere una buona idea.

Quindi prova questo:

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

Grazie, funziona! Questo sembra essere stato un problema.
slipheed,

9

Sfortunatamente, i comandi avviati in background sono impostati dalla shell per ignorare SIGINT e, peggio ancora, non possono ignorarlo trap. Altrimenti, tutto ciò che dovresti fare è

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

Perché process1 e process2 otterrebbero SIGINT quando si preme Ctrl-C poiché fanno parte dello stesso gruppo di processi che è il gruppo di processi in primo piano del terminale.

Il codice sopra funzionerà con pdksh e zsh che al riguardo non sono conformi a POSIX.

Con altre shell, dovresti usare qualcos'altro per ripristinare il gestore predefinito per SIGINT come:

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

o usa un segnale diverso come SIGTERM.


2

Per quelli che vogliono semplicemente uccidere un processo e aspettare che muoia, ma non indefinitamente :

Attende al massimo 60 secondi per tipo di segnale.

Avvertenza: questa risposta non è in alcun modo correlata alla cattura di un segnale di uccisione e all'invio.

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

Seleziona l'app da uccidere per nome o argomenti. Conserva le parentesi per la prima lettera del nome dell'app per evitare di abbinare grep stesso.

Con alcune modifiche, è possibile utilizzare direttamente il PID o un semplice pidof process_nameinvece psdell'istruzione.

Dettagli del codice: Il grep finale è ottenere il PID senza gli spazi finali.


1

Se quello che vuoi è gestire alcuni processi in background, perché non usare le funzioni di controllo del lavoro 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

0

Quindi ho armeggiato anche con questo. In Bash, puoi definire funzioni e fare cose fantasiose con quelle. Io e i miei colleghi usiamo terminator, quindi per le esecuzioni batch normalmente generiamo un sacco di finestre di terminazione se vogliamo vedere l'output (meno elegante delle tmuxschede, ma puoi usare un'interfaccia più simile alla GUI haha).

Ecco la configurazione che mi è venuta in mente, oltre a un esempio di cose che puoi eseguire:

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

Eseguendolo

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

Modifica @Maxim, ho appena visto il tuo suggerimento, che lo rende molto più semplice! Grazie!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.