Perché non posso uccidere un timeout chiamato da uno script Bash con una sequenza di tasti?


11

[Modifica: questo sembra simile ad alcune altre domande che chiedono come uccidere tutti i processi generati - le risposte sembrano essere tutte usare pkill. Quindi il nocciolo della mia domanda potrebbe essere: c'è un modo per propagare Ctrl-C / Z a tutti i processi generati da uno script?]

Quando si chiama un SoX reccon il timeoutcomando da coreutils (discusso qui ), non sembra esserci alcun modo per ucciderlo con una sequenza di tasti una volta che è stato invocato da uno script Bash.

Esempi:

timeout 10 rec test.wav

... può essere ucciso con Ctrl+ Co Ctrl+ Zda bash, ma non quando è stato chiamato dall'interno di uno script.

timeout 10 ping nowhere

... può essere ucciso con Ctrl+ Co Ctrl+ Zda bash e con Ctrl+ Zquando viene eseguito all'interno di uno script.

Posso trovare l'ID del processo e ucciderlo in questo modo, ma perché non posso usare una sequenza di tasti standard? E c'è un modo per strutturare la mia sceneggiatura in modo che io possa?


2
Ctrl + Z non uccide i processi, li mette solo in pausa. Essi potranno continuare a correre se si dà il bgdi fgcomandi. Ad ogni modo, c'è una differenza tra i tuoi esempi 1 ° e 3d?
terdon

Non ho timeoutsul mio sistema, ma uccidere sleepfunziona se viene digitato direttamente sulla riga di comando, proveniente, eseguito o passato esplicitamente nell'interprete
Kevin,

@terdon Grazie, ho chiarito gli esempi.
incontro

Risposte:


18

Tasti di segnale come Ctrl+ Cinviano un segnale a tutti i processi nel gruppo di processi in primo piano .

Nel caso tipico, un gruppo di processi è una pipeline. Ad esempio, in head <somefile | sort, il processo in esecuzione heade il processo in esecuzione si sorttrovano nello stesso gruppo di processi, così come la shell, quindi ricevono tutti il ​​segnale. Quando si esegue un lavoro in background ( somecommand &), quel lavoro si trova nel suo gruppo di processi, quindi premendo Ctrl+ Cnon lo influenza.

Il timeoutprogramma si colloca nel proprio gruppo di processi. Dal codice sorgente:

/* Ensure we're in our own group so all subprocesses can be killed.
   Note we don't just put the child in a separate group as
   then we would need to worry about foreground and background groups
   and propagating signals between them.  */
setpgid (0, 0);

Quando si verifica un timeout, timeoutpassa attraverso il semplice espediente di uccidere il gruppo di processi di cui è membro. Dal momento che si è inserito in un gruppo di processi separato, il suo processo padre non sarà nel gruppo. L'uso di un gruppo di processi qui garantisce che se l'applicazione figlio esegue il fork in più processi, tutti i suoi processi riceveranno il segnale.

Quando si esegue timeoutdirettamente dalla riga di comando e si preme Ctrl+ C, il SIGINT risultante viene ricevuto sia dal timeoutprocesso figlio che dal processo figlio, ma non dalla shell interattiva che è timeoutil processo padre. Quando timeoutviene chiamato da uno script, solo la shell che esegue lo script riceve il segnale: timeoutnon lo riceve poiché si trova in un diverso gruppo di processi.

È possibile impostare un gestore di segnale in uno script di shell con trapincorporato. Sfortunatamente, non è così semplice. Considera questo:

#!/bin/sh
trap 'echo Interrupted at $(date)' INT
date
timeout 5 sleep 10
date

Se si preme Ctrl+ Cdopo 2 secondi, restano comunque in attesa tutti i 5 secondi, quindi si stampa il messaggio "Interrotto". Questo perché la shell si astiene dall'eseguire il codice trap mentre è attivo un lavoro in primo piano.

Per ovviare a questo, eseguire il processo in background. Nel gestore del segnale, chiamare killper inoltrare il segnale al timeoutgruppo di processo.

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid

Molto subdolo - ha funzionato magnificamente! Sono molto più intelligente ora, merci!
Meetar

13

Basandosi sull'ottima risposta fornita da Gilles. Il comando timeout ha un'opzione in primo piano, se usato, un CTRL + C uscirà dal comando timeout.

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date

Questa è un'ottima risposta - più semplice della risposta accettata e ha funzionato bene per me, usando timeoutda GNU coreutils.
RichVel

1

Fondamentalmente Ctrl+ Cinvia un SIGINTsegnale, mentre Ctrl+ Z invia un SIGTSTPsegnale.

SIGTSTPinterrompe il processo e SIGCONTlo continuerà.

Funziona sul processo in primo piano che è stato biforcato sulla riga di comando.

Se il tuo processo è un processo in background dovrai inviare quel segnale ai processi in modo diverso. killlo farà. In teoria un operatore "-" - su quell'uccisione dovrebbe anche segnalare i processi figlio, ma raramente funziona come previsto.

Per ulteriori letture: Unix-Signals


Questo è vero, almeno fino a "raramente funziona come previsto" (che dipende dalle tue aspettative - dovresti leggere sui gruppi di processi), ma completamente irrilevante. La domanda non tradisce alcuna confusione su SIGINT e SIGTSTP.
Gilles 'SO- smetti di essere malvagio' il

0

Migliorare la risposta di @Gilles fornendo un approccio "trova e sostituisci" per gli script esistenti:

  1. Aggiungi questo snippet all'inizio del codice:
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}
  1. Aggiungi (o unisci) il seguente codice nel tuo INTgestore:
pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            #echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
  1. Sostituisci i timeoutcomandi con la my_timeoutfunzione nel tuo script.

Esempio

Ecco uno script di esempio:

#!/bin/bash

# see "killing timeout": /unix//a/57692/65781
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}

cleanup(){
    echo "-----------------------------------------"
    echo "Restoring previous routing table settings"
}

pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
trap cleanup EXIT

echo "Executing 5 temporary timeouts..."
unreachable_ip="192.168.44.5"
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
echo "ctrl+c now to execute cleanup"
my_timeout 9s ping -c 1 "$unreachable_ip" &> /dev/null 
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.