Dlaczego nie mogę zabić limitu czasu wywołanego ze skryptu Bash za pomocą klawisza?

11

[Edycja: Wygląda podobnie do niektórych innych pytań dotyczących sposobu zabicia wszystkich spawnowanych procesów - wszystkie odpowiedzi wydają się używać pkill. Więc rdzeniem mojego pytania może być: Czy istnieje sposób na propagowanie Ctrl-C / Z we wszystkich procesach spawnowanych przez skrypt?]

Podczas wywoływania SoX recza pomocą timeoutpolecenia z coreutils (omówione tutaj ), wydaje się, że nie ma sposobu na zabicie go za pomocą klawisza po wywołaniu go ze skryptu Bash.

Przykłady:

timeout 10 rec test.wav

... można zabić za pomocą Ctrl+ Club Ctrl+ Zz bash, ale nie wtedy, gdy zostanie wywołany z poziomu skryptu.

timeout 10 ping nowhere

... można zabić za pomocą Ctrl+ Club Ctrl+ Zz bash i Ctrl+, Zgdy jest uruchamiany ze skryptu.

Mogę znaleźć identyfikator procesu i zabić go w ten sposób, ale dlaczego nie mogę użyć standardowego naciśnięcia klawisza break? Czy jest jakiś sposób na ustrukturyzowanie mojego skryptu, aby mógł?

meetar
źródło
2
Ctrl + Z nie zabija procesów, tylko je zatrzymuje. Będą biec jeśli dać bgz fgpoleceniami. W każdym razie, czy jest jakaś różnica między twoim 1. a 3d przykładem?
terdon
Nie mam timeoutw swoim systemie, ale zabijanie sleepdziała bez względu na to, czy jest wpisywane bezpośrednio w wierszu polecenia, pozyskiwane, wykonywane lub przekazywane jawnie przez tłumacza
Kevin
@terdon Dzięki, wyjaśniłem przykłady.
meetar

Odpowiedzi:

18

Klawisze sygnałowe, takie jak Ctrl+, Cwysyłają sygnał do wszystkich procesów w grupie procesów pierwszego planu .

W typowym przypadku grupa procesów jest potokiem. Na przykład head <somefile | sortwewnątrz proces działający headi proces działający sortnależą do tej samej grupy procesów, co powłoka, więc wszystkie odbierają sygnał. Po uruchomieniu zadania w tle ( somecommand &) zadanie to znajduje się we własnej grupie procesów, więc naciśnięcie Ctrl+ Cnie ma na niego wpływu.

timeoutProgram umieszcza się we własnej grupie procesowej. Z kodu źródłowego:

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

Gdy nastąpi przerwa, timeoutprzechodzi przez prosty cel polegający na zabiciu grupy procesów, której jest członkiem. Ponieważ znalazł się w osobnej grupie procesów, jego proces nadrzędny nie będzie w tej grupie. Użycie tutaj grupy procesów zapewnia, że ​​jeśli aplikacja potomna rozwidli się na kilka procesów, wszystkie jej procesy otrzymają sygnał.

Kiedy uruchomisz timeoutbezpośrednio w linii poleceń i naciśniesz Ctrl+ C, wynikowy SIGINT zostanie odebrany zarówno przez timeoutproces potomny, jak i przez proces potomny, ale nie przez interaktywną powłokę, która jest timeoutprocesem macierzystym. Kiedy timeoutjest wywoływany ze skryptu, tylko powłoka, która go uruchamia, odbiera sygnał: timeoutnie odbiera go, ponieważ znajduje się w innej grupie procesów.

Możesz ustawić moduł obsługi sygnału w skrypcie powłoki z trapwbudowanym. Niestety nie jest to takie proste. Rozważ to:

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

Jeśli naciśniesz Ctrl+ Cpo 2 sekundach, nadal czeka to pełne 5 sekund, a następnie wydrukujesz komunikat „Przerwany”. Jest tak, ponieważ powłoka powstrzymuje się od uruchamiania kodu pułapki, gdy aktywne jest zadanie pierwszoplanowe.

Aby temu zaradzić, uruchom zadanie w tle. W module obsługi sygnałów wywołaj, killaby przekazać sygnał do timeoutgrupy procesów.

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid
Gilles „SO- przestań być zły”
źródło
Bardzo przebiegły - działał pięknie! Jestem teraz znacznie mądrzejszy, Merci!
meetar
13

Opierając się na doskonałej odpowiedzi udzielonej przez Gillesa. Polecenie timeout ma opcję pierwszoplanową, jeśli zostanie użyte, CTRL + C zamknie polecenie timeout.

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date
Marwan Alsabbagh
źródło
To świetna odpowiedź - prostsza niż odpowiedź zaakceptowana i działała dla mnie dobrze, używając timeoutz GNU coreutils.
RichVel
1

Zasadniczo Ctrl+ Cwysyła SIGINTsygnał, podczas gdy Ctrl+ Z wysyła SIGTSTPsygnał.

SIGTSTPpo prostu zatrzymuje proces i SIGCONTbędzie go kontynuować.

Działa to na pierwszym planie, który został rozwidlony w wierszu polecenia.

Jeśli twój proces jest procesem w tle, będziesz musiał wysłać ten sygnał do procesów w inny sposób. killzrobi to. Teoretycznie operator „-” tego zabicia powinien również sygnalizować procesy potomne, ale rzadko działa to zgodnie z oczekiwaniami.

Do dalszego czytania: Sygnały uniksowe

Nils
źródło
To prawda, przynajmniej do „rzadko działa zgodnie z oczekiwaniami” (zależy to od twoich oczekiwań - powinieneś poczytać o grupach procesów), ale zupełnie nie ma znaczenia. Pytanie nie zdradza żadnych nieporozumień dotyczących SIGINT i SIGTSTP.
Gilles „SO- przestań być zły”
0

Poprawa odpowiedzi @Gilles poprzez zastosowanie podejścia „znajdź i zamień” dla istniejących skryptów:

  1. Dodaj ten fragment kodu na początku kodu:
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. Dodaj (lub scal) następujący kod do modułu INTobsługi:
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. Zamień timeoutpolecenia na my_timeoutfunkcję w skrypcie.

Przykład

Oto przykładowy skrypt:

#!/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 
ceremcem
źródło