我有一个很长的shell脚本,我试图为它添加信号处理。
该脚本的主要任务是运行各种程序,然后清理它们的临时文件。
我想捕获SIGINT。 当捕获到信号时,脚本应该等待当前程序完成执行,然后进行清理并退出。
这是一个MCVE:
#!/bin/sh
stop_this=0
trap 'stop_this=1' 2
while true ; do
result="$(sleep 2 ; echo success)" # run some program
echo "result: '$result'"
echo "Cleaning up..." # clean up temporary files
if [ $stop_this -ne 0 ] ; then
echo 'OK, time to stop this.'
break
fi
done
exit 0
预期结果:
Cleaning up...
result: 'success'
Cleaning up...
^Cresult: 'success'
Cleaning up...
OK, time to stop this.
实际结果:
Cleaning up...
result: 'success'
Cleaning up...
^Cresult: ''
Cleaning up...
OK, time to stop this.
问题是当前正在运行的指令(在这种情况下为result="$(sleep 2 ; echo success)"
)被中断。
我能做什么,它会更像我设置trap '' 2
?
我正在寻找POSIX解决方案或大多数shell解释器支持的解决方案(BusyBox,dash,Cygwin ......)
我已经看到了Prevent SIGINT from closing child process in bash script的答案,但这对我来说并不合适。所有这些解决方案都需要修改不应该被中断的每条线路。我的真实剧本很长,比例子复杂得多。我将不得不修改数百行。
答案 0 :(得分:2)
您需要首先阻止SIGINT转到echo(或者重写您在变量赋值中运行的cmd以忽略SIGINT)。此外,您需要允许变量赋值发生,并且看起来shell在收到SIGINT时正在中止赋值。如果您只担心来自tty的用户生成的SIGINT,则需要将该命令与tty取消关联(例如,将其从前台进程组中取出)并防止SIGINT中止分配。你可以(几乎)完成以下任何一项:
#!/bin/sh
stop_this=0
while true ; do
trap 'stop_this=1' INT
{ sleep 1; echo success > tmpfile; } & # run some program
while ! wait; do : ; done
trap : INT
result=$(cat tmpfile& wait)
echo "result: '$result'"
echo "Cleaning up..." # clean up temporary files
if [ $stop_this -ne 0 ] ; then
echo 'OK, time to stop this.'
break
fi
done
exit 0
如果你担心来自其他来源的SIGINT,你将不得不重新实现sleep
(或我认为sleep
是代理的任何命令)以你想要的方式处理SIGINT 。这里的关键是在后台运行命令并等待它以防止SIGINT转到它并提前终止它。请注意,我们在这里至少打开了2个新的蠕虫病毒。通过循环等待,我们实际上忽略了子命令可能引发的任何错误(我们这样做是为了尝试并实现SIGRESTART),因此可能会挂起。此外,如果SIGINT在cat
期间到达,我们尝试通过在后台运行来阻止cat
中止,但现在变量赋值将被终止并且您将获得原始行为。外壳中的信号处理不干净!但这会让你更接近你想要的目标。
答案 1 :(得分:1)
shell脚本中的Sighandling可能会变得笨拙。这几乎是不可能的 没有C的支持,这样做“正确”。
问题:
result="$(sleep 2 ; echo success)" # run some program
是$()
创建子shell并且在子shell中,不可忽略(trap '' SIGNAL
是你忽略SIGNAL的方式)
信号被重置为其默认配置,SIGINT
将终止该过程
($( )
得到自己的进程,认为它也会收到信号,因为终端生成的SIGINT
是流程组目标)
为了防止这种情况,您可以执行以下操作:
result="$(
trap '' INT #ignore; could get killed right before the trap command
sleep 2; echo success)"
或
result="$( trap : INT; #no-op handler; same problem
sleep 2; while ! echo success; do :; done)"
但是如上所述,在开始之间会有一个小的竞争条件窗口 subshell和信号处理程序的注册期间 子shell可能会被重置为默认的SIGINT信号所杀死。
答案 2 :(得分:0)
来自@PSkocik和@WilliamPursell的答案都帮助我走上正轨。
我有一个完全可行的解决方案。它不漂亮,因为它需要使用外部文件来指示信号没有发生,但除此之外它应该可靠地工作。
#!/bin/sh
touch ./continue
trap 'rm -f ./continue' 2
( # the whole main body of the script is in a separate background process
trap '' 2 # ignore SIGINT
while true ; do
result="$(sleep 2 ; echo success)" # run some program
echo "result: '$result'"
echo "Cleaning up..." # clean up temporary files
if [ ! -e ./continue ] ; then # exit the loop if file "./continue" is deleted
echo 'OK, time to stop this.'
break
fi
done
) & # end of the main body of the script
while ! wait ; do : ; done # wait for the background process to end (ignore signals)
wait $! # wait again to get the exit code
result=$? # exit code of the background process
rm -f ./continue # clean up if the background process ended without a signal
exit $result
编辑:Cygwin中此代码存在一些问题。
有关信号工作的主要功能。
但是,似乎完成的后台进程不会像僵尸一样留在系统中。这使wait $!
不起作用。脚本的退出代码具有不正确的127
值。
解决方法是删除行wait $!
,result=$?
和result=$?
,以便脚本始终返回0。
也可以通过使用另一层子shell来保留正确的错误代码,并将退出代码临时存储在文件中。
答案 3 :(得分:0)
您可能需要调整以下内容:
#!/bin/sh
tmpfile=".tmpfile"
rm -f $tmpfile
trap : INT
# put the action that should not be interrupted in the innermost brackets
# | |
( set -m; (sleep 10; echo success > $tmpfile) & wait ) &
wait # wait will be interrupted by Ctrl+c
while [ ! -r $tmpfile ]; do
echo "waiting for $tmpfile"
sleep 1
done
result=`cat $tmpfile`
echo "result: '$result'"
这似乎也可以与安装自己的SIGINT处理程序(例如mpirun和mpiexec等)的程序一起使用。
答案 4 :(得分:0)
禁止中断程序:
<块引用>trap "" ERR HUP INT QUIT TERM TSTP TTIN TTOU
但是如果一个子命令自己处理陷阱,并且该命令必须真正完成,你需要防止向它传递信号。
对于不介意安装额外命令的 Linux 用户,您可以使用:
<块引用>waitFor [命令]
或者,您可以根据需要将 latest source code of waitFor 调整到您的程序中,或者使用 Gilles' answer 中的代码。尽管这样做的缺点是无法从上游更新中受益。
请注意,其他终端和服务管理器仍然可以终止“命令”。如果您希望服务管理器无法关闭“命令”,则应将其作为具有适当终止模式和终止信号集的服务运行。