在Shell脚本中超时


53

我有一个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是否连接到受支持的终端,但是有时环境在说谎(例如,即使使用调用也要plink设置TERM=xtermTERM=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功能?


2
另请参阅如何为Shell脚本引入超时?便携式解决方案的问题
吉勒斯

Answers:


33

那这个呢:

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

即:运行output-production命令,并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时?)。当交互式shell被杀死时,stderr重定向在那里摆脱了“ Terminated”消息。

经测试zshbashdash。但是老歌呢?

B98建议在Mac OS X,GNU bash 3.2.57或带破折号的Linux上进行以下更改:

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


1.除setsid看起来非标准的以外。


1
我很喜欢这个,我没想到要那样使用流程组。这很简单,没有比赛条件。我需要对它的可移植性进行更多测试,但这看起来像一个赢家。
吉尔(Gilles)'所以

不幸的是,一旦我将其放入脚本中,该操作就会以失败kill 0告终:命令替换中的管道不在其自己的进程组中运行,并且最终也杀死了脚本的调用者。是否有一种可移植的方式将管道强制纳入其自己的进程组?
吉尔(Gilles)'所以

@吉尔斯:哎呀!不能找到一种方法,setprgp()没有setsid现在:-(
斯特凡希门尼斯

1
我真的很喜欢这个窍门,所以我授予赏金。它似乎可以在Linux上运行,但我还没有时间在其他系统上进行测试。
吉尔(Gilles)“所以,别再邪恶了”,

2
@Zac:在原始情况下不可能颠倒顺序,因为只有第一个进程才能访问stdin。
斯特凡希门尼斯

6
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)。


不是那么简单。如果捕获该信号,则许多外壳(破折号,bash,pdksh,zsh…可能除了ATT ksh以外的所有外壳)在等待cat退出时都会忽略该信号。我已经进行了一些尝试,但到目前为止还没有找到我满意的地方。
吉尔(Gilles)'所以

关于可移植性:幸好我到处都有功能。我不确定$!,我认为我很少使用的某些机器没有工作控制,是否可以$!普遍使用?
吉尔(Gilles)'所以

@吉尔斯:嗯,是的。我回想起bash我曾经涉及滥用的一些真正可怕且特定的东西-o monitor。当我写那本书的时候(我的作品在v7中起作用),我在思考真正的古老贝壳。就是说,我认为您可以做另外两件事之一:(1)“无论如何”背景wait $!,或者(2)也发送SIGCLD/ SIGCHLD...,但是在足够老的机器上,后者要么不存在,要么不可移植(前者是System III / V,后者的BSD和V7都没有)。
geekosaur 2011年

@Gilles:$!追溯到V7至少,肯定早一个sh是知道作业控制任何东西(其实,很长一段时间般/bin/shBSD上没做作业控制,你必须运行csh得到它-但$!在那里)。
geekosaur 2011年

我不能作为“任何”背景,我需要它的输出。感谢您提供有关的信息$!
吉尔斯(Gilles)'所以

4

尽管从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 <brad@footle.org>
#                       Make more portable by using the 'CHLD'
#                       signal spec rather than 17.
#    V1.3, Oct 29 2009, Ján Sáreník <jasan@x31.com>
#                       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 <jasan@x31.com>
#                       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

看来这不允许检索命令的输出。参见其他答案中吉尔斯(Gilles)的评论“我无能为力”。
斯蒂芬·吉梅内斯

啊,有趣。我已经修改了脚本(因此它将不再与pixelbeat上的脚本匹配)以将/ dev / stdin重定向到命令中。它似乎在我的测试中有效。
bahamat 2011年

从bash中(奇怪地)除外,这无法使命令从标准输入中读取。</dev/stdin是无人操作。</dev/tty将使其能够从终端读取,这对于我的用例来说已经足够了。
吉尔(Gilles)'所以

@吉尔斯:太好了,我会进行更新。
bahamat 2011年

没有太多的努力就无法工作:我需要检索命令的输出,如果命令在后台,则无法执行该操作。
吉尔(Gilles)“所以,别再邪恶了”,

3

那么(ab)为此使用NC

喜欢;

   $ nc -l 0 2345 | cat &  # output come from here
   $ nc -w 5 0 2345   # input come from here and times out after 5 sec

或汇总为一个命令行;

   $ foo=`nc -l 0 2222 | nc -w 5 0 2222`

最后一个版本虽然看起来很稳定,但实际上在我在linux系统上进行测试时却可以工作-我最好的猜测是它应该在任何系统上都可以工作,并且如果没有输出重定向的变体,也可以解决可移植性。这样做的好处是不涉及任何后台进程。


不够便携。我没有使用nc某些旧的Unix操作系统,也没有使用许多嵌入式Linux。
吉尔(Gilles)'所以

0

在自己的进程组中运行管道的另一种方法是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"

不够便携。我没有使用script某些旧的Unix操作系统,也没有使用许多嵌入式Linux。
吉尔(Gilles)'所以

0

https://unix.stackexchange.com/a/18711中的答案非常好。

我想获得类似的结果,但是不必再次显式调用Shell,因为我想调用现有的Shell函数。

使用bash,可以执行以下操作:

eval 'set -m ; ( ... ) ; '"$(set +o)"

因此,假设我已经有一个shell函数f

f() { date ; kill 0 ; }

echo Before
eval 'set -m ; ( f ) ; '"$(set +o)"
echo After

执行此,我看到:

$ sh /tmp/foo.sh
Before
Mon 14 Mar 2016 17:22:41 PDT
/tmp/foo.sh: line 4: 17763 Terminated: 15          ( f )
After

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.