shell脚本超时

shell脚本超时

我有一个外壳脚本那是从标准输入读取。在极少数情况下,没有人准备好提供输入,并且脚本必须暂停。如果超时,脚本必须执行一些清理代码。最好的方法是什么?

这个脚本必须是非常便携,包括没有C编译器的20世纪unix系统和运行busybox的嵌入式设备,因此Perl、bash、任何编译语言,甚至完整的POSIX.2都不能依赖。特别是$PPIDread -t和 完全符合 POSIX 标准的陷阱不可用。写入临时文件也不包括在内;即使所有文件系统都以只读方式安装,该脚本也可能运行。

只是让事情变得更困难,我也希望剧本合理快速地当它没有超时时。特别是,我也在Windows(主要是Cygwin)中使用脚本,其中fork和exec特别低,所以我想将它们的使用保持在最低限度。

简而言之,我有

trap cleanup 1 2 3 15
foo=`cat`

我想添加一个超时。无法catread内置的替换。如果超时,我想执行该cleanup函数。


背景:该脚本通过打印一些8位字符并比较前后光标位置来猜测终端的编码。脚本的开头测试 stdout 是否已连接到受支持的终端,但有时环境是谎言(例如plinkTERM=xterm即使它被调用也会设置TERM=dumb)。脚本的相关部分如下所示:

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

如何修改脚本,以便如果tr2 秒后没有读取任何输入,脚本就会被终止并执行该cleanup函数?

答案1

那这个呢:

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

也就是说:运行输出生成命令,并sleep在同一进程组中,为它们创建一个进程组。无论哪个命令先返回都会杀死整个进程组。

有人会奇怪:是的,管道没有被使用;使用重定向绕过它。它的唯一目的是让 shell 在同一进程组中运行这两个进程。


正如 Gilles 在评论中指出的那样,这在 shell 脚本中不起作用,因为脚本进程将与两个子进程一起被终止。

强制命令在单独的进程组中运行的一种方法是启动一个新的交互式 shell:

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

但可能有一些警告(例如,当 stdin 不是 tty 时?)。 stderr 重定向是为了在交互式 shell 被终止时消除“已终止”消息。

使用zshbash和进行测试dash。但老歌呢?

B98建议进行以下更改,适用于使用 GNU bash 3.2.57 的 Mac OS X 或使用 dash 的 Linux:

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


1. 除setsid看似非标准的情况外。

答案2

me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1

您已经捕获了 15( 的数字版本SIGTERM,除非另有说明,否则将发送该数字kill),因此您应该已经可以开始了。也就是说,如果您正在查看 POSIX 之前的版本,请注意 shell 函数可能也不存在(它们来自 System V 的 shell)。

答案3

尽管 7.0 版的 coretuils 包含超时命令,但您已经提到了一些环境没有它。幸运的是Pixelbeat.org写了一个超时脚本sh

我之前曾多次使用过它,效果非常好。

http://www.pixelbeat.org/scripts/timeout笔记:下面的脚本与 Pixelbeat.org 上的脚本略有修改,请参阅此答案下面的评论。)

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

答案4

在其自己的进程组中运行管道的另一种方法是sh -c '....'使用命令在伪终端中运行script(隐式应用该setsid函数)。

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

相关内容