【问题标题】:Execute a shell function with timeout执行带有超时的 shell 函数
【发布时间】:2012-04-14 20:04:17
【问题描述】:

为什么会这样

timeout 10s echo "foo bar" # foo bar

但这不会

function echoFooBar {
  echo "foo bar"
}

echoFooBar # foo bar

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

我怎样才能让它发挥作用?

【问题讨论】:

标签: bash function shell timeout


【解决方案1】:

对 TauPan 答案的这个小修改增加了一些有用的保护。如果正在等待的子进程在 sleep $timeout 完成之前已经退出。 kill 命令试图杀死一个不再存在的进程。这可能是无害的,但不能绝对保证没有重新分配相同的 PID。为了避免这种情况,需要进行快速检查以测试子 PID 是否存在,并且其父 PID 是它派生出来的 shell。还试图杀死一个不存在的进程会产生错误,如果不抑制这些错误很容易填满日志。

我还使用了更激进的杀戮-9。这是杀死不是在 shell 命令上而是从文件系统阻塞的进程的唯一方法,例如。 read < named_pipe.
这样做的结果是kill -9 $child 命令将其kill 信号异步发送到进程,从而在调用shell 中生成一条消息。这可以通过重定向wait $child > /dev/null 2>&1 来抑制。对调试有明显的影响。

#!/bin/bash

function child_timeout () {
        child=$!
        timeout=$1
        (
        #trap -- "" SIGINT

        sleep $timeout
        if [ $(ps -o pid= -o comm= --ppid $$ | grep -o $child) ]; then
                kill -9 $child
        fi
        ) &
wait $child > /dev/null 2>&1

}


( tail -f /dev/null ) & child_timeout 10

【讨论】:

    【解决方案2】:

    这一班轮将在 10 秒后退出您的 Bash 会话

    $ TMOUT=10 && echo "foo bar"
    

    【讨论】:

    • 这在 10 秒后完全退出父 bash 会话,这根本不是 OP 所要求的
    【解决方案3】:

    我对@Tiago Lopo 的答案稍作修改,可以处理带有多个参数的命令。我也测试了 TauPan 的解决方案,但如果你在一个脚本中多次使用它就不起作用,而 Tiago 的解决方案却可以。

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

    这是一个功能齐全的脚本,可用于测试上述功能:

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

    例如你可以这样启动:

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

    【讨论】:

      【解决方案4】:

      将我对 Tiago Lopo 的回答的评论变成更易读的形式:

      我认为在最近的子shell 上设置超时更易读,这样我们就不需要评估字符串,并且整个脚本可以被您喜欢的编辑器突出显示为shell。我只是将命令放在带有eval 的子shell 之后生成了一个shell 函数(使用zsh 测试,但应该使用bash):

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

      示例用法:

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

      这样它也可以与 shell 函数一起使用(如果它在后台运行):

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

      【讨论】:

      • 我真的很喜欢这种方法,但如果我在脚本中多次使用它,它只会在第一次起作用。 @Tiago Lopo 的解决方案可以多次使用。
      【解决方案5】:

      这个函数只使用内置函数

      • 根据您的需要,可以考虑评估“$*”而不是直接运行 $@

      • 它使用在作为超时值的第一个 arg 之后指定的命令字符串启动作业并监视作业 pid

      • 它每 1 秒检查一次,bash 支持低至 0.01 的超时,以便可以调整

      • 另外,如果您的脚本需要标准输入,read 应该依赖专用的 fd (exec {tofd}<> <(:))

      • 您可能还需要调整默认为-15 的终止信号(循环内的信号),您可能需要-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
      }
      

      【讨论】:

        【解决方案6】:

        正如 Douglas Leeder 所说,您需要一个单独的进程来发出超时信号。通过将函数导出到子shell并手动运行子shell的解决方法。

        export -f echoFooBar
        timeout 10s bash -c echoFooBar
        

        【讨论】:

          【解决方案7】:
          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 
          

          【讨论】:

            【解决方案8】:

            您可以创建一个函数,该函数允许您执行与超时相同的操作,但也可以用于其他功能:

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

            并且可以如下运行:

            run_cmd "echoFooBar" 10
            

            注意:解决方案来自我的一个问题: Elegant solution to implement timeout for bash commands and functions

            【讨论】:

            • 不应该在wait $child之后杀死最里面的子shell吗?它不会做任何有害的事情(除了等待),但它仍然会继续计数,即使孩子已经完成
            • 这非常有用。我个人发现在脚本中为最近的子进程设置超时而不是执行 eval $cmd 更具可读性。所以对我来说它看起来像这样:timeout_child () { trap -- "" SIGTERM; child=$!; timeout=$1; ( sleep $timeout; kill $child; ) &amp; wait $child; } 用法:( while true; do echo -n .; sleep 0.1; done) &amp; timeout_child 2
            【解决方案9】:

            还有一个内联替代方案也启动了 bash shell 的子进程:

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

            【讨论】:

            • Here Document 子进程不知道父进程函数(即“找不到命令”错误),因此请确保 export -f parent_func(或 set -o allexport 提前为所有函数),在父 shell 进程。
            【解决方案10】:

            如果您只是想为整个现有脚本添加超时作为附加选项,您可以让它测试超时选项,然后让它在没有该选项的情况下递归调用它。

            example.sh:

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

            在没有超时的情况下运行此脚本:

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

            以一分钟超时运行它:

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

            【讨论】:

              【解决方案11】:

              timeout 是一个命令 - 所以它在你的 bash shell 的子进程中执行。因此它无法访问您在当前 shell 中定义的函数。

              给出的命令timeout 是作为超时的子进程执行的——你的shell 的孙子进程。

              您可能会感到困惑,因为 echo 既是一个内置的 shell,又是一个单独的命令。

              你可以做的是把你的函数放在它自己的脚本文件中,chmod它是可执行的,然后用timeout执行它。

              或者 fork,在子 shell 中执行您的函数 - 在原始进程中,监视进度,如果耗时过长则终止子进程。

              【讨论】:

              • 感谢您的解决方案!但是,由于我想将超时添加为现有脚本的附加选项,因此为超时功能拥有一个自己的文件会非常不方便。这是唯一的解决方案吗?
              • @speendo 考虑到timeout 通过向进程发送信号来杀死进程 - 这是您只能对进程执行的操作。因此,无论您在超时情况下运行什么,都必须是它自己的进程。
              • @speendo 另请注意,bash 是(AFAIK)单线程的,那么如果线程正在执行您的函数,那么超时功能可以做什么?
              猜你喜欢
              • 1970-01-01
              • 2021-09-16
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-04-11
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多