Warum kann ich eine von einem Bash-Skript aufgerufene Zeitüberschreitung nicht mit einem Tastendruck beenden?

11

[Bearbeiten: Dies sieht ähnlich aus wie einige andere Fragen, bei denen gefragt wird, wie alle erzeugten Prozesse beendet werden sollen. Die Antworten scheinen alle darin zu bestehen, pkill zu verwenden. Der Kern meiner Frage könnte also sein: Gibt es eine Möglichkeit, Strg-C / Z auf alle Prozesse zu übertragen, die von einem Skript erzeugt werden?]

Wenn Sie ein SoX recmit dem timeoutBefehl von coreutils ( hier beschrieben ) aufrufen , scheint es keine Möglichkeit zu geben, es mit einem Tastendruck zu beenden, sobald es aus einem Bash-Skript heraus aufgerufen wurde.

Beispiele:

timeout 10 rec test.wav

... kann mit Ctrl+ Coder Ctrl+ Zvon bash getötet werden, aber nicht, wenn es aus einem Skript heraus aufgerufen wurde.

timeout 10 ping nowhere

... kann mit Ctrl+ Coder Ctrl+ Zvon bash und mit Ctrl+ getötet werden, Zwenn es aus einem Skript heraus ausgeführt wird.

Ich kann die Prozess-ID finden und auf diese Weise beenden, aber warum kann ich keinen Standard-Unterbrechungstastenanschlag verwenden? Und gibt es eine Möglichkeit, mein Skript so zu strukturieren, dass ich es kann?

Meetar
quelle
2
Strg + Z beendet keine Prozesse, pausiert sie nur. Sie werden weiter ausgeführt, wenn Sie die Befehle bgof fgeingeben. Gibt es einen Unterschied zwischen Ihrem 1. und 3. Beispiel?
Terdon
Ich habe nicht timeoutauf meinem System, aber das Töten sleepfunktioniert, unabhängig davon, ob es direkt in der Befehlszeile eingegeben, bezogen, ausgeführt oder explizit durch den Interpreter geleitet wird
Kevin
@terdon Danke, ich habe die Beispiele geklärt.
Treffen

Antworten:

18

Signaltasten wie Ctrl+ Csenden ein Signal an alle Prozesse im Vordergrund Prozessgruppe .

Im typischen Fall ist eine Prozessgruppe eine Pipeline. In befinden sich beispielsweise head <somefile | sortder laufende Prozess headund der laufende Prozess sortin derselben Prozessgruppe wie die Shell, sodass alle das Signal empfangen. Wenn Sie einen Job im Hintergrund ( somecommand &) ausführen , befindet sich dieser Job in einer eigenen Prozessgruppe, sodass das Drücken von Ctrl+ Ckeinen Einfluss darauf hat.

Das timeoutProgramm stellt sich in eine eigene Prozessgruppe. Aus dem Quellcode:

/* 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);

Wenn eine Zeitüberschreitung auftritt, timeoutwird das einfache Mittel durchlaufen, um die Prozessgruppe zu beenden, zu der sie gehört. Da es sich in eine separate Prozessgruppe gestellt hat, befindet sich sein übergeordneter Prozess nicht in der Gruppe. Durch die Verwendung einer Prozessgruppe wird hier sichergestellt, dass alle Prozesse das Signal empfangen, wenn sich die untergeordnete Anwendung in mehrere Prozesse aufteilt.

Wenn Sie timeoutdirekt in der Befehlszeile ausführen und Ctrl+ drücken C, wird das resultierende SIGINT sowohl vom timeoutals auch vom untergeordneten Prozess empfangen , jedoch nicht von der interaktiven Shell, die timeoutder übergeordnete Prozess ist. Wenn timeoutes von einem Skript aufgerufen wird, empfängt nur die Shell, die das Skript ausführt, das Signal: Es timeoutwird nicht empfangen , da es sich in einer anderen Prozessgruppe befindet.

Sie können einen Signalhandler in einem Shell-Skript mit dem trapeingebauten festlegen. Leider ist es nicht so einfach. Bedenken Sie:

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

Wenn Sie nach 2 Sekunden Ctrl+ drücken C, wartet dies noch die vollen 5 Sekunden und druckt dann die Meldung „Unterbrochen“. Dies liegt daran, dass die Shell den Trap-Code nicht ausführt, während ein Vordergrundjob aktiv ist.

Um dies zu beheben, führen Sie den Job im Hintergrund aus. Rufen Sie killim Signalhandler auf , um das Signal an die timeoutProzessgruppe weiterzuleiten.

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid
Gilles 'SO - hör auf böse zu sein'
quelle
Sehr hinterhältig - hat wunderbar funktioniert! Ich bin jetzt viel schlauer, Merci!
Treffen
13

Aufbauend auf der hervorragenden Antwort von Gilles. Der Timeout-Befehl verfügt über eine Vordergrundoption. Wenn er verwendet wird, beendet STRG + C den Timeout-Befehl.

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
Marwan Alsabbagh
quelle
Dies ist eine großartige Antwort - einfacher als die akzeptierte Antwort und hat für mich mit timeoutGNU-Coreutils gut funktioniert .
RichVel
1

Grundsätzlich sendet Ctrl+ Cein SIGINTSignal, während Ctrl+ Z ein SIGTSTPSignal sendet .

SIGTSTPstoppt einfach den Prozess, a SIGCONTwird ihn fortsetzen.

Dies funktioniert mit dem Vordergrundprozess, der in der Befehlszeile gegabelt wurde.

Wenn Ihr Prozess ein Hintergrundprozess ist, müssen Sie dieses Signal auf andere Weise an die Prozesse senden. killwird das machen. Theoretisch sollte ein "-" - Operator für diesen Kill auch untergeordnete Prozesse signalisieren, aber dies funktioniert selten wie erwartet.

Zur weiteren Lektüre: Unix-Signale

Nils
quelle
Dies ist wahr, zumindest bis zu „funktioniert selten wie erwartet“ (was von Ihren Erwartungen abhängt - Sie sollten sich über Prozessgruppen informieren), aber völlig irrelevant. Die Frage verrät keine Verwirrung über SIGINT und SIGTSTP.
Gilles 'SO - hör auf böse zu sein'
0

Verbesserung der Antwort von @Gilles durch Bereitstellung eines "Find and Replace " -Ansatzes für vorhandene Skripte:

  1. Fügen Sie dieses Snippet am Anfang Ihres Codes hinzu:
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. Fügen Sie Ihrem INTHandler den folgenden Code hinzu (oder führen Sie ihn zusammen) :
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. Ersetzen Sie timeoutBefehle durch my_timeoutFunktionen in Ihrem Skript.

Beispiel

Hier ist ein Beispielskript:

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