Führen Sie eine Shell-Funktion mit Timeout aus

76

Warum sollte das funktionieren?

timeout 10s echo "foo bar" # foo bar

aber das würde nicht

function echoFooBar {
  echo "foo bar"
}

echoFooBar # foo bar

timeout 10s echoFooBar # timeout: failed to run command `echoFooBar': No such file or directory

und wie kann ich es zum Laufen bringen?

Speendo
quelle

Antworten:

64

timeoutist ein Befehl - wird also in einem Unterprozess Ihrer Bash-Shell ausgeführt. Daher hat es keinen Zugriff auf Ihre Funktionen, die in Ihrer aktuellen Shell definiert sind.

Der timeoutgegebene Befehl wird als Teilprozess des Timeouts ausgeführt - ein Enkelprozess Ihrer Shell.

Sie könnten verwirrt sein, da echosowohl eine Shell integriert als auch ein separater Befehl ist.

Was Sie tun können, ist, Ihre Funktion in eine eigene Skriptdatei zu stellen, sie so zu ändern, dass sie ausführbar ist, und sie dann mit auszuführen timeout.

Alternativ können Sie Ihre Funktion in einer Unter-Shell ausführen - und im ursprünglichen Prozess den Fortschritt überwachen und den Unterprozess beenden, wenn er zu lange dauert.

Douglas Leeder
quelle
Vielen Dank für Ihre Lösung! Da ich jedoch eine Zeitüberschreitung als zusätzliche Option für ein vorhandenes Skript hinzufügen möchte, wäre es ziemlich unpraktisch, nur für die Zeitüberschreitungsfunktion eine eigene Datei zu haben. Ist das die einzige Lösung?
Speendo
6
@speendo Bedenken Sie, dass timeoutProzesse beendet werden, indem Signale gesendet werden - das können Sie nur für Prozesse tun. Daher muss alles, was Sie mit Timeout ausführen, ein eigener Prozess sein.
Douglas Leeder
2
@speendo Beachten Sie auch, dass bash (AFAIK) Single-Threaded ist. Was kann also die Timeout-Funktionalität bewirken, wenn der Thread Ihre Funktion ausführt?
Douglas Leeder
62

Wie Douglas Leeder sagte, benötigen Sie einen separaten Prozess, um eine Zeitüberschreitung zu signalisieren. Problemumgehung durch Exportieren der Funktion in Unterschalen und manuelles Ausführen der Unterschale.

export -f echoFooBar
timeout 10s bash -c echoFooBar
user3132194
quelle
28

Es gibt eine Inline-Alternative, die auch einen Unterprozess der Bash-Shell startet:


timeout 10s bash <<EOT
function echoFooBar {
  echo foo
}

echoFooBar
sleep 20
EOT

Eduardo Lago Aguilar
quelle
Hier kennt der Unterprozess " Dokument" die übergeordneten Prozessfunktionen nicht (dh den Fehler "Befehl nicht gefunden"). Stellen Sie daher sicher, dass export -f parent_func(oder set -o allexportfür alle Funktionen im Voraus) der übergeordnete Shell-Prozess aktiviert ist.
Noam Manos
10

Sie können eine Funktion erstellen, mit der Sie das gleiche wie Timeout ausführen können, aber auch für andere Funktionen:

function run_cmd { 
    cmd="$1"; timeout="$2";
    grep -qP '^\d+$' <<< $timeout || timeout=10

    ( 
        eval "$cmd" &
        child=$!
        trap -- "" SIGTERM 
        (       
                sleep $timeout
                kill $child 2> /dev/null 
        ) &     
        wait $child
    )
}

Und könnte wie folgt laufen:

run_cmd "echoFooBar" 10

Hinweis: Die Lösung ergab sich aus einer meiner Fragen: Elegante Lösung zur Implementierung eines Timeouts für Bash-Befehle und -Funktionen

Tiago Lopo
quelle
Sollte die innerste Unterschale nicht auch danach getötet werden wait $child? es tut nichts schädliches (außer zu warten), aber es zählt immer noch, selbst wenn das Kind fertig ist
phil294
Das ist sehr nützlich. Ich persönlich finde es in einem Skript besser lesbar, eine Zeitüberschreitung für den letzten Unterprozess zu haben, als eval $ cmd auszuführen. Also für mich sieht es so aus: timeout_child () { trap -- "" SIGTERM; child=$!; timeout=$1; ( sleep $timeout; kill $child; ) & wait $child; } Und die Verwendung: ( while true; do echo -n .; sleep 0.1; done) & timeout_child 2
TauPan
6

Wenn Sie nur Timeout als zusätzliche Option für das gesamte vorhandene Skript hinzufügen möchten, können Sie es auf die Timeout-Option testen lassen und es dann ohne diese Option selbst rekursiv aufrufen.

example.sh:

#!/bin/bash
if [ "$1" == "-t" ]; then
  timeout 1m $0 $2
else
  #the original script
  echo $1
  sleep 2m
  echo YAWN...
fi

Ausführen dieses Skripts ohne Zeitüberschreitung:

$./example.sh -other_option # -other_option
                            # YAWN...

Ausführen mit einer Zeitüberschreitung von einer Minute:

$./example.sh -t -other_option # -other_option
Superole
quelle
4
function foo(){
    for i in {1..100};
    do 
        echo $i;  
        sleep 1;
    done;
}

cat <( foo ) # Will work 
timeout 3 cat <( foo ) # Will Work 
timeout 3 cat <( foo ) | sort # Wont work, As sort will fail 
cat <( timeout 3 cat <( foo ) ) | sort -r # Will Work 
Hemant Patel
quelle
1

Diese Funktion verwendet nur integrierte Funktionen

  • Vielleicht sollten Sie "$ *" auswerten, anstatt $ @ direkt auszuführen, je nach Ihren Anforderungen

  • Es startet einen Job mit der Befehlszeichenfolge, die nach dem ersten Argument angegeben wird, das der Timeout-Wert ist, und überwacht die Job-PID

  • Es überprüft alle 1 Sekunden, Bash unterstützt Timeouts bis zu 0,01, so dass optimiert werden kann

  • Auch wenn Ihr Skript stdin benötigt, readsollte es sich auf ein dediziertes fd ( exec {tofd}<> <(:)) verlassen

  • Auch sollten Sie das Kill - Signal (die innerhalb der Schleife) zu optimieren , die standardmäßig auf -15, möchten Sie vielleicht-9

## forking is evil
timeout() {
    to=$1; shift
    $@ & local wp=$! start=0
     while kill -0 $wp; do
        read -t 1
        start=$((start+1))
        if [ $start -ge $to ]; then
            kill $wp && break
        fi
    done
}
aufreißen
quelle
1

Setzen Sie meinen Kommentar zu Tiago Lopos Antwort in eine besser lesbare Form:

Ich denke, es ist besser lesbar, der neuesten Subshell eine Zeitüberschreitung aufzuerlegen. Auf diese Weise müssen wir keine Zeichenfolge auswerten, und das gesamte Skript kann von Ihrem bevorzugten Editor als Shell hervorgehoben werden. Ich setze einfach die Befehle, nachdem die Subshell mit evalin eine Shell-Funktion übergegangen ist (getestet mit zsh, sollte aber mit bash funktionieren):

timeout_child () {
    trap -- "" SIGTERM
    child=$!
    timeout=$1
    (
            sleep $timeout
            kill $child
    ) &
    wait $child
}

Anwendungsbeispiel:

( while true; do echo -n .; sleep 0.1; done) & timeout_child 2

Und auf diese Weise funktioniert es auch mit einer Shell-Funktion (wenn sie im Hintergrund ausgeführt wird):

 print_dots () {
     while true
     do
         sleep 0.1
         echo -n .
     done
 }


 > print_dots & timeout_child 2
 [1] 21725
 [3] 21727
 ...................[1]    21725 terminated  print_dots
 [3]  + 21727 done       ( sleep $timeout; kill $child; )
TauPan
quelle
1
Ich mag den Ansatz wirklich, aber wenn ich ihn mehrmals in einem Skript verwende, funktioniert er nur beim ersten Mal. Die Lösung von @Tiago Lopo funktioniert mehrmals.
CristianCantoro
1

Ich habe eine geringfügige Änderung der Antwort von @Tiago Lopo, die Befehle mit mehreren Argumenten verarbeiten kann. Ich habe auch die Lösung von TauPan getestet, aber sie funktioniert nicht, wenn Sie sie mehrmals in einem Skript verwenden, während dies bei Tiago der Fall ist.

function timeout_cmd { 
  local arr
  local cmd
  local timeout

  arr=( "$@" )

  # timeout: first arg
  # cmd: the other args
  timeout="${arr[0]}"
  cmd=( "${arr[@]:1}" )

  ( 
    eval "${cmd[@]}" &
    child=$!

    echo "child: $child"
    trap -- "" SIGTERM 
    (       
      sleep "$timeout"
      kill "$child" 2> /dev/null 
    ) &     
    wait "$child"
  )
}

Hier ist ein voll funktionsfähiges Skript, mit dem Sie die obige Funktion testen können:

$ ./test_timeout.sh -h
Usage:
  test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT]
  test_timeout.sh -h

Test timeout_cmd function.

Options:
  -n              Dry run, do not actually sleep. 
  -r REPEAT       Reapeat everything multiple times [default: 1].
  -s SLEEP_TIME   Sleep for SLEEP_TIME seconds [default: 5].
  -t TIMEOUT      Timeout after TIMEOUT seconds [default: no timeout].

Zum Beispiel können Sie wie folgt starten:

$ ./test_timeout.sh -r 2 -s 5 -t 3
Try no: 1
  - Set timeout to: 3
child: 2540
    -> retval: 143
    -> The command timed out
Try no: 2
  - Set timeout to: 3
child: 2593
    -> retval: 143
    -> The command timed out
Done!
#!/usr/bin/env bash

#shellcheck disable=SC2128
SOURCED=false && [ "$0" = "$BASH_SOURCE" ] || SOURCED=true

if ! $SOURCED; then
  set -euo pipefail
  IFS=$'\n\t'
fi

#################### helpers
function check_posint() {
  local re='^[0-9]+$'
  local mynum="$1"
  local option="$2"

  if ! [[ "$mynum" =~ $re ]] ; then
     (echo -n "Error in option '$option': " >&2)
     (echo "must be a positive integer, got $mynum." >&2)
     exit 1
  fi

  if ! [ "$mynum" -gt 0 ] ; then
     (echo "Error in option '$option': must be positive, got $mynum." >&2)
     exit 1
  fi
}
#################### end: helpers

#################### usage
function short_usage() {
  (>&2 echo \
"Usage:
  test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT]
  test_timeout.sh -h"
  )
}

function usage() {
  (>&2 short_usage )
  (>&2 echo \
"
Test timeout_cmd function.

Options:
  -n              Dry run, do not actually sleep. 
  -r REPEAT       Reapeat everything multiple times [default: 1].
  -s SLEEP_TIME   Sleep for SLEEP_TIME seconds [default: 5].
  -t TIMEOUT      Timeout after TIMEOUT seconds [default: no timeout].
")
}
#################### end: usage

help_flag=false
dryrun_flag=false
SLEEP_TIME=5
TIMEOUT=-1
REPEAT=1

while getopts ":hnr:s:t:" opt; do
  case $opt in
    h)
      help_flag=true
      ;;    
    n)
      dryrun_flag=true
      ;;
    r)
      check_posint "$OPTARG" '-r'

      REPEAT="$OPTARG"
      ;;
    s)
      check_posint "$OPTARG" '-s'

      SLEEP_TIME="$OPTARG"
      ;;
    t)
      check_posint "$OPTARG" '-t'

      TIMEOUT="$OPTARG"
      ;;
    \?)
      (>&2 echo "Error. Invalid option: -$OPTARG.")
      (>&2 echo "Try -h to get help")
      short_usage
      exit 1
      ;;
    :)
      (>&2 echo "Error.Option -$OPTARG requires an argument.")
      (>&2 echo "Try -h to get help")
      short_usage
      exit 1
      ;;
  esac
done

if $help_flag; then
  usage
  exit 0
fi

#################### utils
if $dryrun_flag; then
  function wrap_run() {
    ( echo -en "[dry run]\\t" )
    ( echo "$@" )
  }
else
  function wrap_run() { "$@"; }
fi

# Execute a shell function with timeout
# https://stackoverflow.com/a/24416732/2377454
function timeout_cmd { 
  local arr
  local cmd
  local timeout

  arr=( "$@" )

  # timeout: first arg
  # cmd: the other args
  timeout="${arr[0]}"
  cmd=( "${arr[@]:1}" )

  ( 
    eval "${cmd[@]}" &
    child=$!

    echo "child: $child"
    trap -- "" SIGTERM 
    (       
      sleep "$timeout"
      kill "$child" 2> /dev/null 
    ) &     
    wait "$child"
  )
}
####################

function sleep_func() {
  local secs
  local waitsec

  waitsec=1
  secs=$(($1))
  while [ "$secs" -gt 0 ]; do
   echo -ne "$secs\033[0K\r"
   sleep "$waitsec"
   secs=$((secs-waitsec))
  done

}

command=("wrap_run" \
         "sleep_func" "${SLEEP_TIME}"
         )

for i in $(seq 1 "$REPEAT"); do
  echo "Try no: $i"

  if [ "$TIMEOUT" -gt 0 ]; then
    echo "  - Set timeout to: $TIMEOUT"
    set +e
    timeout_cmd "$TIMEOUT" "${command[@]}"
    retval="$?"
    set -e

    echo "    -> retval: $retval"
    # check if (retval % 128) == SIGTERM (== 15)
    if [[ "$((retval % 128))" -eq 15 ]]; then
      echo "    -> The command timed out"
    fi
  else
    echo "  - No timeout"
    "${command[@]}"
    retval="$?"
  fi
done

echo "Done!"

exit 0
CristianCantoro
quelle
-1

Dieser eine Liner beendet Ihre Bash-Sitzung nach 10 Sekunden

$ TMOUT=10 && echo "foo bar"
Michel Hua
quelle
1
Dies beendet die Eltern-Bash-Sitzung vollständig nach 10s, was überhaupt nicht das ist, was das OP verlangt hat
Danielpops