Przekroczono limit czasu w skrypcie powłoki

53

Mam skrypt powłoki, który odczytuje ze standardowego wejścia . W rzadkich przypadkach nikt nie będzie gotowy do wprowadzenia danych, a skrypt musi upłynąć limit czasu . W przypadku przekroczenia limitu czasu skrypt musi wykonać kod czyszczenia. Jak najlepiej to zrobić?

Skrypt ten musi być bardzo przenośny , w tym do systemów unixowych XX wieku bez kompilatora C oraz do urządzeń wbudowanych z uruchomionym busyboxem, więc nie można polegać na Perlu, bash, dowolnym języku kompilowanym, a nawet pełnym POSIX.2. W szczególności $PPID, read -ti pułapki idealnie POSIX nie są dostępne. Zapisywanie do pliku tymczasowego jest również wykluczone; skrypt może działać, nawet jeśli wszystkie systemy plików są zamontowane tylko do odczytu.

Żeby było trudniej, chcę, aby skrypt był dość szybki, gdy nie upłynie limit czasu. W szczególności używam również skryptu w systemie Windows (głównie w Cygwin), gdzie rozwidlenie i exec są szczególnie niskie, więc chcę ograniczyć ich użycie do minimum.

W skrócie, mam

trap cleanup 1 2 3 15
foo=`cat`

i chcę dodać limit czasu. Nie mogę wymienić catz readzabudowy. W przypadku przekroczenia limitu czasu chcę wykonać tę cleanupfunkcję.


Tło: ten skrypt zgaduje kodowanie terminala, drukując jakieś 8-bitowe znaki i porównując pozycję kursora przed i po. Początek skryptu testuje, że stdout jest podłączony do obsługiwanego terminala, ale czasami środowisko leży (np. plinkUstawia, TERM=xtermnawet jeśli jest wywoływaneTERM=dumb ). Odpowiednia część skryptu wygląda następująco:

text='Éé'  # UTF-8; shows up as Ãé on a latin1 terminal
csi='␛['; dsr_cpr="${csi}6n"; dsr_ok="${csi}5n"  # ␛ is an escape character
stty_save=`stty -g`
cleanup () { stty "$stty_save"; }
trap 'cleanup; exit 120' 0 1 2 3 15     # cleanup code
stty eol 0 eof n -echo                # Input will end with `0n`
# echo-n is a function that outputs its argument without a newline
echo-n "$dsr_cpr$dsr_ok"              # Ask the terminal to report the cursor position
initial_report=`tr -dc \;0123456789`  # Expect ␛[42;10R␛[0n for y=42,x=10
echo-n "$text$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
cleanup
# Compute and return initial_x - final_x

Jak mogę zmodyfikować skrypt, aby jeśli trnie odczytał żadnych danych wejściowych po 2 sekundach, został zabity, a skrypt wykonuje cleanupfunkcję?

Gilles „SO- przestań być zły”
źródło
2
Zobacz także Jak wprowadzić limit czasu dla skryptów powłoki? dla mniej przenośnych rozwiązań
Gilles „SO- przestań być zły”

Odpowiedzi:

33

A co z tym:

foo=`{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1`

To znaczy: uruchom komendę produkującą dane wyjściowe i sleepw tej samej grupie procesów, dla nich tylko grupy procesów. Którekolwiek polecenie zwróci pierwsze, zabija całą grupę procesów.

Czy ktoś mógłby się zastanawiać: Tak, rura nie jest używana; jest ominięty przy użyciu przekierowań. Jedynym celem tego jest, aby powłoka uruchamiała dwa procesy w tej samej grupie procesów.


Jak zauważył Gilles w swoim komentarzu, nie zadziała to w skrypcie powłoki, ponieważ proces skryptu zostałby zabity wraz z dwoma podprocesami.

Jednym ze sposobów¹ wymuszenia uruchomienia polecenia w oddzielnej grupie procesów jest uruchomienie nowej interaktywnej powłoki:

#!/bin/sh
foo=`sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null`
[ -n "$foo" ] && echo got: "$foo" || echo timeouted

Ale mogą być z tym zastrzeżenia (np. Kiedy stdin nie jest tty?). Przekierowanie stderr ma za zadanie pozbyć się komunikatu „Zakończony”, gdy interaktywna powłoka zostanie zabita.

Testowane z zsh, bashi dash. Ale co z oldies?

B98 sugeruje następującą zmianę, działającą na Mac OS X, z GNU bash 3.2.57 lub Linux z dash:

foo=`sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }'`

-
1. inne niż te, setsidktóre wydają się niestandardowe.

Stéphane Gimenez
źródło
1
Podoba mi się to, nie myślałem o takim użyciu grupy procesów. To dość proste i nie ma warunków do wyścigu. Muszę go jeszcze przetestować pod kątem przenośności, ale wygląda to na zwycięzcę.
Gilles „SO- przestań być zły”
Niestety, nie udaje się to spektakularnie, gdy tylko wstawię to do skryptu: potok w podstawianiu komend nie działa we własnej grupie procesów i kill 0ostatecznie zabija również wywołującego skrypt. Czy istnieje przenośny sposób na wtłaczanie potoku do własnej grupy procesów?
Gilles „SO- przestań być zły”
@Gilles: Eek! nie mógł znaleźć sposób, aby setprgp()bez setsidteraz :-(
Stéphane Giménez
1
Naprawdę podoba mi się ta sztuczka, więc przyznam nagrodę. Wygląda na to, że działa na Linuksie, nie miałem jeszcze czasu przetestować go na innych systemach.
Gilles „SO- przestań być zły”,
2
@Zac: Nie jest możliwe odwrócenie kolejności w oryginalnym przypadku, ponieważ tylko pierwszy proces uzyskuje dostęp do standardowego wejścia.
Stéphane Gimenez
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Jesteś już w pułapce 15 (wersja numeryczna SIGTERM, która killwysyła, o ile nie podano inaczej), więc powinieneś już iść. To powiedziawszy, jeśli patrzysz na pre-POSIX, pamiętaj, że funkcje powłoki również mogą nie istnieć (pochodzą z powłoki Systemu V).

geekozaur
źródło
To nie takie proste. Jeśli złapiesz sygnał w pułapkę, wiele powłok (kreska, bash, pdksh, zsh… prawdopodobnie wszystkie oprócz ATT ksh) ignorują go, czekając na catwyjście. Już trochę eksperymentowałem, ale jak dotąd nie znalazłem niczego, z czego jestem zadowolony.
Gilles „SO- przestań być zły”
Na przenośność: na szczęście mam funkcje wszędzie. Nie jestem pewien $!, myślę, że niektóre z tych maszyn, których używam rzadko nie mają kontroli zadań, są $!powszechnie dostępne?
Gilles 'SO - przestań być zły'
@Gilles: Hm, tak. Pamiętam kilka naprawdę ohydnych i bashspecyficznych rzeczy, które kiedyś zrobiłem, w tym nadużycia -o monitor. Kiedy o tym pisałem, myślałem o naprawdę pradawnych muszlach (zadziałało w wersji 7). To powiedziawszy, myślę, że możesz zrobić jedną z dwóch innych rzeczy: (1) tło „cokolwiek” i wait $!, lub (2) również wysłać SIGCLD/ SIGCHLD... ale na wystarczająco starych maszynach to drugie nie istnieje lub nie można go przenosić (pierwszym z nich jest System III / V, ten drugi BSD i V7 nie miały żadnego).
geekozaur
@Gilles: $!wraca co najmniej do V7 i na pewno wyprzedza coś shtakiego, co wiedziało cokolwiek na temat kontroli zadań (tak naprawdę przez długi czas /bin/shna BSD nie kontrolował pracy; musiałeś biec, cshaby ją zdobyć - ale $!był tam ).
geekozaur
Nie mogę w tle „cokolwiek”, potrzebuję jego wyników. Dzięki za informacje o $!.
Gilles „SO- przestań być zły”
4

Chociaż coretuils od wersji 7.0 zawiera polecenie limitu czasu, o którym wspomniałeś w niektórych środowiskach, które go nie mają. Na szczęście pixelbeat.org ma napisany skrypt przekroczenia limitu czasu sh.

Używałem go już kilkakrotnie i działa bardzo dobrze.

http://www.pixelbeat.org/scripts/timeout ( Uwaga: Poniższy skrypt został nieco zmodyfikowany w stosunku do tego na pixelbeat.org, zobacz komentarze poniżej tej odpowiedzi.)

#!/bin/sh

# Execute a command with a timeout

# Author:
#    http://www.pixelbeat.org/
# Notes:
#    Note there is a timeout command packaged with coreutils since v7.0
#    If the timeout occurs the exit status is 124.
#    There is an asynchronous (and buggy) equivalent of this
#    script packaged with bash (under /usr/share/doc/ in my distro),
#    which I only noticed after writing this.
#    I noticed later again that there is a C equivalent of this packaged
#    with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
#    V1.0, Nov  3 2006, Initial release
#    V1.1, Nov 20 2007, Brad Greenlee <[email protected]>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <[email protected]>
#                       Even though this runs under dash,ksh etc.
#                       it doesn't actually timeout. So enforce bash for now.
#                       Also change exit on timeout from 128 to 124
#                       to match coreutils.
#    V2.0, Oct 30 2009, Ján Sáreník <[email protected]>
#                       Rewritten to cover compatibility with other
#                       Bourne shell implementations (pdksh, dash)

if [ "$#" -lt "2" ]; then
    echo "Usage:   `basename $0` timeout_in_seconds command" >&2
    echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
    exit 1
fi

cleanup()
{
    trap - ALRM               #reset handler to default
    kill -ALRM $a 2>/dev/null #stop timer subshell if running
    kill $! 2>/dev/null &&    #kill last job
      exit 124                #exit with 124 if it was running
}

watchit()
{
    trap "cleanup" ALRM
    sleep $1& wait
    kill -ALRM $$
}

watchit $1& a=$!         #start the timeout
shift                    #first param was timeout for sleep
trap "cleanup" ALRM INT  #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$?    #start the job wait for it and save its return value
kill -ALRM $a            #send ALRM signal to watchit
wait $a                  #wait for watchit to finish cleanup
exit $RET                #return the value
bahamat
źródło
Wygląda na to, że nie pozwala to na uzyskanie danych wyjściowych polecenia. Zobacz komentarz „Nie mogę w tle”, cokolwiek „” Gillesa w drugiej odpowiedzi.
Stéphane Gimenez
Ach, ciekawe. Zmodyfikowałem skrypt (aby nie był już zgodny ze skryptem pixelbeat), aby przekierować / dev / stdin do polecenia. Wydaje się, że działa w moich testach.
bahamat
Nie działa to na odczytanie polecenia ze standardowego wejścia, z wyjątkiem (dziwnie) w bash. </dev/stdinjest zakazem. </dev/ttyumożliwiłby odczyt z terminala, co jest wystarczające dla mojego przypadku użycia.
Gilles „SO- przestań być zły”
@Giles: spoko, zrobię tę aktualizację.
bahamat
Nie może to działać bez znacznie większego wysiłku: muszę pobrać dane wyjściowe polecenia i nie mogę tego zrobić, jeśli polecenie jest w tle.
Gilles „SO- przestań być zły”
3

Co z (ab) użyć do tego NC?

Lubić;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

Lub zwinięte w jedną linię poleceń;

   $ foo=`nc -l 0 2222 | nc -w 5 0 2222`

Ostatnia wersja, choć wyglądająca dziwnie, faktycznie działa, kiedy testuję ją na systemie Linux - moim zdaniem przypuszcza się, że powinna ona działać na każdym systemie, a jeśli nie, to pewna odmiana przekierowania wyjścia może rozwiązać problem przenośności. korzyścią tutaj jest to, że nie są zaangażowane żadne procesy w tle.

Soren
źródło
Niewystarczająco przenośny. Nie mam ncna jakimś starym Uniksie, ani na wielu wbudowanych Linuksach.
Gilles „SO- przestań być zły”
0

Innym sposobem uruchomienia potoku we własnej grupie procesów jest uruchomienie sh -c '....'w pseudo-terminalu za pomocą scriptpolecenia (które domyślnie stosuje setsidfunkcję).

#!/bin/sh
stty -echo -onlcr
# GNU script
foo=`script -q -c 'sh -c "{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }" 3>&1 2>/dev/null' /dev/null`
# FreeBSD script
#foo=`script -q /dev/null sh -c '{ cat 1>&3; kill 0; } | { sleep 5; kill 0; }' 3>&1 2>/dev/null`
stty echo onlcr
echo "foo: $foo"


# alternative without: stty -echo -onlcr
# cr=`printf '\r'`
# foo=`script -q -c 'sh -c "{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null"' /dev/null | sed -e "s/${cr}$//" -ne 'p;N'`  # GNU
# foo=`script -q /dev/null sh -c '{ { cat 1>&3; kill 0; } | { sleep 5; kill 0; } } 3>&1 2>/dev/null' | sed -e "s/${cr}$//" -ne 'p;N'`  # FreeBSD
# echo "foo: $foo"
pete
źródło
Niewystarczająco przenośny. Nie mam scriptna jakimś starym Uniksie, ani na wielu wbudowanych Linuksach.
Gilles „SO- przestań być zły”
0

Odpowiedź w https://unix.stackexchange.com/a/18711 jest bardzo miła.

Chciałem osiągnąć podobny wynik, ale bez konieczności jawnego wywoływania powłoki ponownie, ponieważ chciałem wywołać istniejące funkcje powłoki.

Za pomocą bash można wykonać następujące czynności:

eval 'set -m ; ( ... ) ; '"$(set +o)"

Załóżmy, że mam już funkcję powłoki f:

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

Wykonując to widzę:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
Hrabia
źródło
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
Cory Schwartz
źródło
To nie działa z tego samego powodu, co odpowiedź geekausora . Muszę uzyskać wynik polecenia zewnętrznego, a sygnał jest ignorowany podczas działania tego polecenia.
Gilles „SO- przestań być zły”