Zeitüberschreitung in einem Shell-Skript

53

Ich habe ein Shell-Skript , das von der Standardeingabe liest . In seltenen Fällen ist niemand bereit, Eingaben zu machen, und das Skript muss eine Zeitüberschreitung aufweisen . Im Falle einer Zeitüberschreitung muss das Skript einen Bereinigungscode ausführen. Wie geht das am besten?

Dieses Skript muss sehr portabel sein , einschließlich für Unix-Systeme des 20. Jahrhunderts ohne C-Compiler und für eingebettete Geräte, auf denen busybox ausgeführt wird. Perl, bash, jede kompilierte Sprache und sogar das vollständige POSIX.2 können daher nicht verwendet werden. Insbesondere $PPID, read -tund perfekt POSIX-kompatibele Fallen sind nicht verfügbar. Das Schreiben in eine temporäre Datei ist ebenfalls ausgeschlossen. Das Skript wird möglicherweise auch dann ausgeführt, wenn alle Dateisysteme schreibgeschützt bereitgestellt sind.

Um die Sache noch schwieriger zu machen, möchte ich auch, dass das Skript einigermaßen schnell ist, wenn keine Zeitüberschreitung auftritt. Insbesondere verwende ich das Skript auch in Windows (hauptsächlich in Cygwin), wo Fork und Exec besonders niedrig sind, so dass ich ihre Verwendung auf ein Minimum beschränken möchte.

Kurz gesagt, ich habe

trap cleanup 1 2 3 15
foo=`cat`

und ich möchte eine Auszeit hinzufügen. Ich kann nicht catmit dem readeingebauten ersetzen . Im Falle eines Timeouts möchte ich die cleanupFunktion ausführen .


Hintergrund: Dieses Skript errät die Codierung des Terminals, indem es einige 8-Bit-Zeichen druckt und die Cursorposition davor und danach vergleicht. Der Anfang des Skripts testet, dass stdout mit einem unterstützten Terminal verbunden ist, aber manchmal lügt die Umgebung (z. B. plinksetzt, TERM=xtermauch wenn es mit aufgerufen wirdTERM=dumb ). Der relevante Teil des Skripts sieht folgendermaßen aus:

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

Wie kann ich das Skript so ändern, dass tres beendet wird und das Skript die cleanupFunktion ausführt , wenn nach 2 Sekunden keine Eingabe gelesen wurde ?

Gilles 'SO - hör auf böse zu sein'
quelle
2
Siehe auch Wie führe ich ein Timeout für Shell-Skripte ein? für weniger tragbare Lösungen
Gilles 'SO - hör auf, böse zu sein'

Antworten:

33

Was ist damit:

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

Das heißt: Führen Sie den Befehl zur Ausgabeerzeugung aus und sleepin derselben Prozessgruppe eine Prozessgruppe, die nur für sie bestimmt ist. Welcher Befehl zuerst zurückgibt, beendet die gesamte Prozessgruppe.

Würde sich jemand fragen: Ja, die Pfeife wird nicht benutzt; Es wird mit den Umleitungen umgangen. Der einzige Zweck besteht darin, dass die Shell die beiden Prozesse in derselben Prozessgruppe ausführt.


Wie Gilles in seinem Kommentar ausführte, funktioniert dies in einem Shell-Skript nicht, da der Skriptprozess zusammen mit den beiden Unterprozessen abgebrochen würde.

Eine Möglichkeit¹, die Ausführung eines Befehls in einer separaten Prozessgruppe zu erzwingen, besteht darin, eine neue interaktive Shell zu starten:

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

Dies kann jedoch zu Einschränkungen führen (z. B. wenn stdin kein tty ist?). Die stderr-Umleitung dient dazu, die Meldung "Abgebrochen" zu entfernen, wenn die interaktive Shell beendet wird.

Getestet mit zsh, bashund dash. Aber was ist mit Oldies?

B98 schlägt die folgende Änderung vor, die unter Mac OS X mit GNU Bash 3.2.57 oder Linux mit Bindestrich funktioniert:

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

-
1. andere als setsiddie, die nicht dem Standard entsprechen.

Stéphane Gimenez
quelle
1
Mir gefällt das ganz gut, ich hatte nicht daran gedacht, eine Prozessgruppe auf diese Weise zu verwenden. Es ist ziemlich einfach und es gibt keine Rennbedingungen. Ich muss es noch auf Portabilität testen, aber das sieht nach einem Gewinner aus.
Gilles 'SO - hör auf böse zu sein'
Leider schlägt dies spektakulär fehl, sobald ich dies in ein Skript eingefügt habe: Die Pipeline in der Befehlsersetzung wird nicht in einer eigenen Prozessgruppe ausgeführt und kill 0beendet auch den Aufrufer des Skripts. Gibt es eine tragbare Möglichkeit, die Pipeline in eine eigene Prozessgruppe zu zwingen?
Gilles 'SO - hör auf böse zu sein'
@ Gilles: Eek! konnte vorerst keinen Weg setprgp()ohne finden setsid:-(
Stéphane Gimenez
1
Ich mag den Trick wirklich, also gebe ich das Kopfgeld. Es scheint unter Linux zu funktionieren, ich hatte noch keine Zeit, es auf anderen Systemen zu testen.
Gilles 'SO - hör auf, böse zu sein'
2
@Zac: Es ist nicht möglich, die Reihenfolge im ursprünglichen Fall umzukehren, da nur der erste Prozess Zugriff auf stdin erhält.
Stéphane Gimenez
6
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

Sie haben bereits 15 abgefangen (die numerische Version von SIGTERM, die, killsofern nicht anders angegeben, gesendet wird). Sie sollten also bereits bereit sein, loszulegen. Beachten Sie jedoch, dass die Shell-Funktionen möglicherweise nicht vorhanden sind (sie stammen aus der Shell von System V).

Geekosaurier
quelle
Es ist nicht so einfach. Wenn Sie das Signal abfangen, ignorieren viele Shells (Bindestrich, Bash, Pdksh, Zsh ... wahrscheinlich alle außer ATT Ksh) es, während sie auf catdas Beenden warten . Ich habe bereits ein wenig experimentiert, aber noch nichts gefunden, mit dem ich zufrieden bin.
Gilles 'SO- hör auf böse zu sein'
Zur Portabilität: Zum Glück habe ich überall Funktionen. Ich bin mir nicht sicher $!, ich denke, einige der Maschinen, die ich verwende, haben keine Jobkontrolle und sind $!universell verfügbar?
Gilles 'SO- hör auf böse zu sein'
@ Gilles: Hm, yeh. Ich erinnere mich an einige wirklich abscheuliche und bashspezifische Dinge, die ich einmal mit Missbrauch getan habe -o monitor. Als ich das schrieb, dachte ich an wirklich alte Muscheln (es funktionierte in Version 7). Das heißt, ich denke, Sie können eines von zwei anderen Dingen tun: (1) Hintergrund "was auch immer" und wait $!(2) auch senden SIGCLD/ SIGCHLD... aber auf alt genug Maschinen ist die letztere entweder nicht vorhanden oder nicht portierbar (die erste ist System III / V, letztere BSD und V7 hatten keine).
Geekosaurier
@Gilles: Geht $!zumindest auf V7 zurück und stammt sicherlich von einem shVorgänger, der etwas über Jobkontrolle wusste (tatsächlich hat /bin/shBSD lange Zeit keine Jobkontrolle durchgeführt; man musste rennen csh, um es zu bekommen - war aber $!da ).
Geekosaurier
Ich kann nicht "was auch immer" im Hintergrund, ich brauche seine Ausgabe. Danke für die Info über $!.
Gilles 'SO- hör auf böse zu sein'
4

Obwohl coretuils ab Version 7.0 einen Timeout-Befehl enthält, haben Sie einige Umgebungen erwähnt, in denen dies nicht möglich ist. Zum Glück hat pixelbeat.org ein Timeout-Skript geschrieben sh.

Ich habe es schon mehrmals benutzt und es funktioniert sehr gut.

http://www.pixelbeat.org/scripts/timeout ( Hinweis: Das folgende Skript wurde gegenüber dem auf pixelbeat.org geringfügig geändert. Siehe die Kommentare unter dieser Antwort.)

#!/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
quelle
Es scheint, dass dies nicht erlaubt, die Ausgabe des Befehls abzurufen. Siehe "Ich kann nicht", was auch immer "Kommentar von Gilles in der anderen Antwort.
Stéphane Gimenez
Ah, interessant. Ich habe das Skript geändert (damit es nicht mehr mit dem auf pixelbeat übereinstimmt), um / dev / stdin in den Befehl umzuleiten. Es scheint in meinen Tests zu funktionieren.
Bahamat
Dies funktioniert nicht, um den Befehl von der Standardeingabe lesen zu lassen, außer (seltsamerweise) in Bash. </dev/stdinist ein No-Op. </dev/ttywürde es ermöglichen, vom Terminal zu lesen, was für meinen Anwendungsfall gut genug ist.
Gilles 'SO - hör auf böse zu sein'
@Giles: cool, ich mache das Update.
Bahamat
Dies kann nicht ohne wesentlich mehr Aufwand funktionieren: Ich muss die Ausgabe des Befehls abrufen, und das kann ich nicht, wenn sich der Befehl im Hintergrund befindet.
Gilles 'SO- hör auf böse zu sein'
3

Was ist mit (ab) NC dafür?

Mögen;

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

Oder in einer einzigen Befehlszeile zusammengefasst;

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

Die letzte Version, obwohl sie seltsam aussieht, funktioniert tatsächlich, wenn ich sie auf einem Linux-System teste. Ich vermute, dass sie auf jedem System funktionieren sollte, und wenn nicht, kann eine Variation der Ausgabeumleitung die Portabilität beeinträchtigen. Vorteil hierbei ist, dass keine Hintergrundprozesse involviert sind.

Soren
quelle
Nicht tragbar genug. Ich habe weder ncauf einigen alten Unix-Boxen noch auf vielen Embedded-Linux.
Gilles 'SO- hör auf böse zu sein'
0

Eine andere Möglichkeit, die Pipeline in einer eigenen Prozessgruppe auszuführen, besteht darin, sie sh -c '....'mit dem scriptBefehl (der die setsidFunktion implizit anwendet ) in einem Pseudoterminal auszuführen .

#!/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"
Peter
quelle
Nicht tragbar genug. Ich habe weder scriptauf einigen alten Unix-Boxen noch auf vielen Embedded-Linux.
Gilles 'SO- hör auf böse zu sein'
0

Die Antwort unter https://unix.stackexchange.com/a/18711 ist sehr nett.

Ich wollte ein ähnliches Ergebnis erzielen, aber ohne die Shell erneut explizit aufrufen zu müssen, weil ich vorhandene Shell-Funktionen aufrufen wollte.

Mit bash ist Folgendes möglich:

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

Angenommen, ich habe bereits eine Shell-Funktion f:

f() { date ; kill 0 ; }

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

Wenn ich das ausführe, sehe ich:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After
Graf
quelle
-2
timeout_handler () { echo Timeout. goodbye.;  exit 1; }
trap timeout_handler ALRM
( sleep 60; kill -ALRM $$ ) &
Cory Schwartz
quelle
Dies funktioniert nicht aus dem gleichen Grund wie die Antwort von Geekausor . Ich muss die Ausgabe eines externen Befehls erhalten, und das Signal wird ignoriert, während dieser Befehl ausgeführt wird.
Gilles 'SO- hör auf böse zu sein'