developer tip

시간 초과로 셸 함수 실행

copycodes 2021. 1. 6. 08:31
반응형

시간 초과로 셸 함수 실행


이게 왜 작동할까요

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

어떻게 작동시킬 수 있습니까?


timeout명령이므로 bash 셸의 하위 프로세스에서 실행됩니다. 따라서 현재 셸에 정의 된 함수에 액세스 할 수 없습니다.

이 명령 timeout은 타임 아웃의 하위 프로세스-쉘의 손자 프로세스로 실행됩니다.

echo셸이 내장되어 있고 별도의 명령이 있기 때문에 혼동 될 수 있습니다 .

당신이 할 수있는 일은 함수를 자체 스크립트 파일에 넣고 실행 가능하도록 chmod 한 다음 timeout.

또는 하위 셸에서 함수를 실행하고 원래 프로세스에서 진행 상황을 모니터링하여 너무 오래 걸리면 하위 프로세스를 종료합니다.


Douglas Leeder가 말했듯이 신호를 보낼 시간 제한에 대한 별도의 프로세스가 필요합니다. 기능을 서브 쉘로 내보내고 서브 쉘을 수동으로 실행하여 해결하십시오.

export -f echoFooBar
timeout 10s bash -c echoFooBar

bash 쉘의 하위 프로세스를 시작하는 인라인 대안도 있습니다.


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

echoFooBar
sleep 20
EOT


타임 아웃과 동일하게 수행 할 수있는 함수를 만들 수 있지만 다른 함수에도 사용할 수 있습니다.

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

참고 : 솔루션은 내 질문 중 하나에서 나왔습니다. bash 명령 및 함수에 대한 시간 제한을 구현하는 우아한 솔루션


기존 스크립트 전체에 대한 추가 옵션으로 시간 제한을 추가하려는 경우 시간 제한 옵션을 테스트 한 다음 해당 옵션없이 자체 재귀 적으로 호출하도록 할 수 있습니다.

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

1 분 제한 시간으로 실행 :

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

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 

이 함수는 내장 기능 만 사용합니다.

  • 필요에 따라 $ @를 직접 실행하는 대신 "$ *"를 평가하는 것이 좋습니다.

  • 제한 시간 값인 첫 번째 인수 다음에 지정된 명령 문자열로 작업을 시작하고 작업 pid를 모니터합니다.

  • 1 초마다 확인하고 bash는 0.01까지의 시간 제한을 지원하므로 조정할 수 있습니다.

  • 또한 스크립트에 stdin이 필요한 경우 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
}

Tiago Lopo의 답변에 대한 내 의견을 더 읽기 쉬운 형식으로 넣으십시오.

가장 최근의 서브 쉘에 타임 아웃을 적용하는 것이 더 읽기 쉽다고 생각합니다. 이렇게하면 문자열을 평가할 필요가 없으며 전체 스크립트를 좋아하는 편집기에서 쉘로 강조 표시 할 수 있습니다. 하위 eval셸이 셸 함수에 생성 된 후에 명령을 간단히 입력합니다 (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

그리고 이런 식으로 쉘 기능 (백그라운드에서 실행되는 경우)과 함께 작동합니다.

 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의 답변을 약간 수정했습니다. 나는 또한 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

이 라이너는 10 초 후 Bash 세션을 종료합니다.

$ TMOUT=10 && echo "foo bar"

ReferenceURL : https://stackoverflow.com/questions/9954794/execute-a-shell-function-with-timeout

반응형