Execute a shell function with timeout

前端 未结 10 1082
死守一世寂寞
死守一世寂寞 2020-12-01 00:40

Why would this work

timeout 10s echo \"foo bar\" # foo bar

but this wouldn\'t

function echoFooBar {
  echo \"foo bar\"
}

e         


        
相关标签:
10条回答
  • 2020-12-01 01:05

    if you just want to add timeout as an additional option for the entire existing script, you can make it test for the timeout-option, and then make it call it self recursively without that option.

    example.sh:

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

    running this script without timeout:

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

    running it with a one minute timeout:

    $./example.sh -t -other_option # -other_option
    
    0 讨论(0)
  • 2020-12-01 01:06
    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 
    
    0 讨论(0)
  • 2020-12-01 01:08

    I have a slight modification of @Tiago Lopo's answer that can handle commands with multiple arguments. I've also tested TauPan's solution, but it does not work if you use it multiple times in a script, while Tiago's does.

    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"
      )
    }
    

    Here's a fully functional script thant you can use to test the function above:

    $ ./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].
    

    For example you cnal launch like this:

    $ ./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
    
    0 讨论(0)
  • 2020-12-01 01:10

    You can create a function which would allow you to do the same as timeout but also for other functions:

    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
        )
    }
    

    And could run as below:

    run_cmd "echoFooBar" 10
    

    Note: The solution came from one of my questions: Elegant solution to implement timeout for bash commands and functions

    0 讨论(0)
  • 2020-12-01 01:12

    Putting my comment to Tiago Lopo's answer into more readable form:

    I think it's more readable to impose a timeout on the most recent subshell, this way we don't need to eval a string and the whole script can be highlighted as shell by your favourite editor. I simply put the commands after the subshell with eval has spawned into a shell-function (tested with zsh, but should work with bash):

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

    Example usage:

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

    And this way it also works with a shell function (if it runs in the background):

     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; )
    
    0 讨论(0)
  • 2020-12-01 01:16

    As Douglas Leeder said you need a separate process for timeout to signal to. Workaround by exporting function to subshells and running subshell manually.

    export -f echoFooBar
    timeout 10s bash -c echoFooBar
    
    0 讨论(0)
提交回复
热议问题