bash:可能需要双Ctrl-c来退出脚本?

时间:2021-08-23 07:37:50

End-goal: BASH script that is waiting for background jobs to finish does not abort on first Ctrl-c; instead, it requires a second Ctrl-c to quit.

结束目标:等待后台作业完成的BASH脚本不会在第一个Ctrl-c中止;相反,它需要第二个Ctrl-c退出。

I'm well aware of how the BASH-builtin trap works. You can either:

我很清楚BASH内置陷阱的工作原理。你可以:

  1. Use it to ignore a signal altogether (e.g., trap '' 2) ... or

    用它来完全忽略一个信号(例如,陷阱''2)......或

  2. Use it to have arbitrary commands executed before a signals original function is allowed to happen (e.g., trap cmd 2, where cmd is run before the parent script will be interrupted due to SIGINT)

    在允许信号原始函数发生之前使用它来执行任意命令(例如,陷阱cmd 2,其中cmd在父脚本因SIGINT而中断之前运行)

So the question boils down to this:

所以问题归结为:

How can I effectively combine 1 & 2 together, i.e., prevent the end-result a signal would lead to (1 -- e.g., stop script cancelling due to SIGINT) while also making that signal cause something else (2 -- e.g., increment a counter, check the counter and conditionally either print a warning or exit).

如何有效地将1和2组合在一起,即防止信号导致的最终结果(例如,由于SIGINT而停止脚本取消),同时还使该信号导致其他内容(2 - 例如,增量柜台,检查柜台,有条件地打印警告或退出)。

Put more simply:

更简单地说:

How can I make a signal do something else entirely; not just insert a job before it does its thing.

我怎样才能发出信号呢?不只是在它完成它之前插入一个工作。

Here's some example code to demonstrate what I'm aiming at; however, it of course doesn't work -- because trap can only do 1 or 2 from above.

这是一些示例代码,用于演示我的目标;然而,它当然不起作用 - 因为陷阱只能从上面做1或2。

#!/bin/bash
declare -i number_of_times_trap_triggered
cleanup_bg_jobs() {
    number_of_times_trap_triggered+=1
    if [[ ${number_of_times_trap_triggered} -eq 1 ]]; then
        echo "There are background jobs still running"
        echo "Hit Ctrl-c again to cancel all bg jobs & quit"
    else
        echo "Aborting background jobs"
        for pid in ${bg_jobs}; do echo "  Killing ${pid}"; kill -9 ${pid}; done
    fi
}
f() { sleep 5m; }
trap cleanup_bg_jobs 2
bg_jobs=
for job in 1 2 3; do
    f &
    bg_jobs+=" $!"
done
wait

So this is the output you end up getting when you press Ctrl-c once.

因此,当您按Ctrl-c一次时,这是您最终获得的输出。

[rsaw:~]$ ./zax 
^CThere are background jobs still running
Hit Ctrl-c again to cancel all bg jobs & quit
[rsaw:~]$ ps axf|tail -6 
24569 pts/3    S      0:00 /bin/bash ./zax
24572 pts/3    S      0:00  \_ sleep 5m
24570 pts/3    S      0:00 /bin/bash ./zax
24573 pts/3    S      0:00  \_ sleep 5m
24571 pts/3    S      0:00 /bin/bash ./zax
24574 pts/3    S      0:00  \_ sleep 5m

Of course I could modify that to clean up the jobs on the first Ctrl-c, but that's not what I want. I want to stop BASH from quiting after the first trap is triggered ... until it's triggered a second time.

当然我可以修改它来清理第一个Ctrl-c上的作业,但这不是我想要的。我想在触发第一个陷阱后停止BASH退出......直到第二次触发它为止。

PS: Target platform is Linux (I couldn't care less about POSIX compliance) with BASH v4+

PS:目标平台是Linux(我不太关心POSIX合规性)与BASH v4 +

4 个解决方案

#1


4  

I did something like this here and it mostly breaks down to this:

我在这里做了类似的事情,它主要分解为:

    ATTEMPT=0
handle_close() {
    if [ $ATTEMPT -eq 0 ]; then
        ATTEMPT=1
        echo "Shutdown."
    else
        echo "Already tried to shutdown. Killing."
        exit 0
    fi
}
trap handle_close SIGINT SIGTERM

You can set a variable in your handler that you can check again next time it is trapped.

您可以在处理程序中设置一个变量,您可以在下次捕获时再次检查该变量。

#2


3  

I had a slightly different use case, and wanted to leave the solution here, as Google led me to this topic. You can keep running a command, and allow the user to restart it with one CTRL+C, and kill it with double CTRL+C in the following manner:

我有一个稍微不同的用例,并希望将解决方案留在这里,因为谷歌引导我进入这个主题。您可以继续运行命令,并允许用户使用一个CTRL + C重新启动它,并使用双CTRL + C以下列方式将其终止:

trap_ctrlC() {
    echo "Press CTRL-C again to kill. Restarting in 2 second"
    sleep 2 || exit 1
}

trap trap_ctrlC SIGINT SIGTERM

while true; do  
    ... your stuff here ...
done

#3


1  

A colleague (Grega) just gave me a solution which ... well I can't believe I didn't think of it first.

一位同事(格雷加)刚刚给了我一个解决方案......我无法相信我没有先想到它。

"My approach would ... be to lay it off for long enough, possibly forever, using a function that just never returns or something (another wait?), so that the second handler can do its job properly."

“我的方法是......使用一个永远不会返回的函数或另一个函数(另一个等待?)来永久地搁置它,可能是永远的,以便第二个处理程序可以正常工作。”

For the record, wait would not work here. (Recursive.) However, adding a sleep command to my original code's cleanup_bg_jobs() function would take care of it .. but would lead to orphaned processes. So I leveraged process groups to ensure that all children of the script really do get killed. Simplified example for posterity:

为了记录,等待在这里不起作用。 (递归。)但是,在我的原始代码的cleanup_bg_jobs()函数中添加一个sleep命令会处理它...但会导致孤立的进程。因此,我利用进程组来确保脚本的所有子项确实被杀死。后人的简化示例:

#!/bin/bash
declare -i count=
handle_interrupt() {
    count+=1
    if [[ ${count} -eq 1 ]]; then
        echo "Background jobs still running"
        echo "Hit Ctrl-c again to cancel all bg jobs & quit"
        sleep 1h
    else
        echo "Aborting background jobs"
        pkill --pgroup 0
    fi
}
f() { tload &>/dev/null; }
trap handle_interrupt 2
for job in 1 2 3; do
    f &
done
wait

#4


0  

  1. Use it to have arbitrary commands executed before a signals original function is allowed to happen (e.g., trap cmd 2, where cmd is run before the parent script will be interrupted due to SIGINT)
  2. 在允许信号原始函数发生之前使用它来执行任意命令(例如,陷阱cmd 2,其中cmd在父脚本因SIGINT而中断之前运行)

The italicized part of the above is incorrect. A trap handler is run instead of letting SIGINT (or whatever) interrupt the process. More accurately:

上面的斜体部分是不正确的。运行陷阱处理程序而不是让SIGINT(或其他)中断进程。更精确地:

  • the default action of SIGINT (and of most, but not all, other signals) is to terminate the process
  • SIGINT(以及大多数,但不是全部,其他信号)的默认操作是终止进程

  • trap "command" SIGINT causes command to be run instead of (not as well as) the default action
  • trap“command”SIGINT导致运行命令而不是(不如)默认操作

So with your SIGINT handler installed, the SIGINT doesn't interrupt the entire script. But it does interrupt the wait command. When the trap handler finishes, the script resumes after the wait, i.e. it falls off the end and exits normally. You can see this by adding some debugging code:

因此,安装了SIGINT处理程序后,SIGINT不会中断整个脚本。但它会中断wait命令。陷阱处理程序完成后,脚本会在等待后恢复,即它从结束时退出并正常退出。你可以通过添加一些调试代码来看到这个:

echo Waiting
wait
echo Back from wait
exit 55                   # Arbitrary value that wouldn't otherwise occur

This version produces the following:

此版本产生以下内容:

$ foo
Waiting
^CThere are background jobs still running
Hit Ctrl-c again to cancel all bg jobs & quit
back from wait
$ echo $?
55
$ 

What you need to do is repeat the wait after the handler returns. This version:

您需要做的是在处理程序返回后重复等待。这个版本:

#!/bin/bash
declare -i number_of_times_trap_triggered
cleanup_bg_jobs() {
    number_of_times_trap_triggered+=1
    if [[ ${number_of_times_trap_triggered} -eq 1 ]]; then
        echo "There are background jobs still running"
        echo "Hit Ctrl-c again to cancel all bg jobs & quit"
    else
        echo "Aborting background jobs"
        for pid in ${bg_jobs}; do echo "  Killing ${pid}"; kill -9 ${pid}; done
        exit 1
    fi
}
f() { sleep 5m; }
trap cleanup_bg_jobs 2
bg_jobs=
for job in 1 2 3; do
    f &
    bg_jobs+=" $!"
done

while [ 1 ]; do
    echo Waiting
    wait
    echo Back from wait
done

does as you requested:

按你的要求做:

$ ./foo
Waiting
^CThere are background jobs still running
Hit Ctrl-c again to cancel all bg jobs & quit
Back from wait
Waiting
^CAborting background jobs
  Killing 24154
  Killing 24155
  Killing 24156
$ 

Notes:

  • I've left in the debugging stuff; obviously you'd remove it in production
  • 我已经离开了调试的东西;显然你会在生产中删除它

  • The handler now does exit 1 after killing off the subprocesses. That's what breaks out of the infinite main loop
  • 处理程序现在在终止子进程后退出1。这就是突破无限主循环的原因

#1


4  

I did something like this here and it mostly breaks down to this:

我在这里做了类似的事情,它主要分解为:

    ATTEMPT=0
handle_close() {
    if [ $ATTEMPT -eq 0 ]; then
        ATTEMPT=1
        echo "Shutdown."
    else
        echo "Already tried to shutdown. Killing."
        exit 0
    fi
}
trap handle_close SIGINT SIGTERM

You can set a variable in your handler that you can check again next time it is trapped.

您可以在处理程序中设置一个变量,您可以在下次捕获时再次检查该变量。

#2


3  

I had a slightly different use case, and wanted to leave the solution here, as Google led me to this topic. You can keep running a command, and allow the user to restart it with one CTRL+C, and kill it with double CTRL+C in the following manner:

我有一个稍微不同的用例,并希望将解决方案留在这里,因为谷歌引导我进入这个主题。您可以继续运行命令,并允许用户使用一个CTRL + C重新启动它,并使用双CTRL + C以下列方式将其终止:

trap_ctrlC() {
    echo "Press CTRL-C again to kill. Restarting in 2 second"
    sleep 2 || exit 1
}

trap trap_ctrlC SIGINT SIGTERM

while true; do  
    ... your stuff here ...
done

#3


1  

A colleague (Grega) just gave me a solution which ... well I can't believe I didn't think of it first.

一位同事(格雷加)刚刚给了我一个解决方案......我无法相信我没有先想到它。

"My approach would ... be to lay it off for long enough, possibly forever, using a function that just never returns or something (another wait?), so that the second handler can do its job properly."

“我的方法是......使用一个永远不会返回的函数或另一个函数(另一个等待?)来永久地搁置它,可能是永远的,以便第二个处理程序可以正常工作。”

For the record, wait would not work here. (Recursive.) However, adding a sleep command to my original code's cleanup_bg_jobs() function would take care of it .. but would lead to orphaned processes. So I leveraged process groups to ensure that all children of the script really do get killed. Simplified example for posterity:

为了记录,等待在这里不起作用。 (递归。)但是,在我的原始代码的cleanup_bg_jobs()函数中添加一个sleep命令会处理它...但会导致孤立的进程。因此,我利用进程组来确保脚本的所有子项确实被杀死。后人的简化示例:

#!/bin/bash
declare -i count=
handle_interrupt() {
    count+=1
    if [[ ${count} -eq 1 ]]; then
        echo "Background jobs still running"
        echo "Hit Ctrl-c again to cancel all bg jobs & quit"
        sleep 1h
    else
        echo "Aborting background jobs"
        pkill --pgroup 0
    fi
}
f() { tload &>/dev/null; }
trap handle_interrupt 2
for job in 1 2 3; do
    f &
done
wait

#4


0  

  1. Use it to have arbitrary commands executed before a signals original function is allowed to happen (e.g., trap cmd 2, where cmd is run before the parent script will be interrupted due to SIGINT)
  2. 在允许信号原始函数发生之前使用它来执行任意命令(例如,陷阱cmd 2,其中cmd在父脚本因SIGINT而中断之前运行)

The italicized part of the above is incorrect. A trap handler is run instead of letting SIGINT (or whatever) interrupt the process. More accurately:

上面的斜体部分是不正确的。运行陷阱处理程序而不是让SIGINT(或其他)中断进程。更精确地:

  • the default action of SIGINT (and of most, but not all, other signals) is to terminate the process
  • SIGINT(以及大多数,但不是全部,其他信号)的默认操作是终止进程

  • trap "command" SIGINT causes command to be run instead of (not as well as) the default action
  • trap“command”SIGINT导致运行命令而不是(不如)默认操作

So with your SIGINT handler installed, the SIGINT doesn't interrupt the entire script. But it does interrupt the wait command. When the trap handler finishes, the script resumes after the wait, i.e. it falls off the end and exits normally. You can see this by adding some debugging code:

因此,安装了SIGINT处理程序后,SIGINT不会中断整个脚本。但它会中断wait命令。陷阱处理程序完成后,脚本会在等待后恢复,即它从结束时退出并正常退出。你可以通过添加一些调试代码来看到这个:

echo Waiting
wait
echo Back from wait
exit 55                   # Arbitrary value that wouldn't otherwise occur

This version produces the following:

此版本产生以下内容:

$ foo
Waiting
^CThere are background jobs still running
Hit Ctrl-c again to cancel all bg jobs & quit
back from wait
$ echo $?
55
$ 

What you need to do is repeat the wait after the handler returns. This version:

您需要做的是在处理程序返回后重复等待。这个版本:

#!/bin/bash
declare -i number_of_times_trap_triggered
cleanup_bg_jobs() {
    number_of_times_trap_triggered+=1
    if [[ ${number_of_times_trap_triggered} -eq 1 ]]; then
        echo "There are background jobs still running"
        echo "Hit Ctrl-c again to cancel all bg jobs & quit"
    else
        echo "Aborting background jobs"
        for pid in ${bg_jobs}; do echo "  Killing ${pid}"; kill -9 ${pid}; done
        exit 1
    fi
}
f() { sleep 5m; }
trap cleanup_bg_jobs 2
bg_jobs=
for job in 1 2 3; do
    f &
    bg_jobs+=" $!"
done

while [ 1 ]; do
    echo Waiting
    wait
    echo Back from wait
done

does as you requested:

按你的要求做:

$ ./foo
Waiting
^CThere are background jobs still running
Hit Ctrl-c again to cancel all bg jobs & quit
Back from wait
Waiting
^CAborting background jobs
  Killing 24154
  Killing 24155
  Killing 24156
$ 

Notes:

  • I've left in the debugging stuff; obviously you'd remove it in production
  • 我已经离开了调试的东西;显然你会在生产中删除它

  • The handler now does exit 1 after killing off the subprocesses. That's what breaks out of the infinite main loop
  • 处理程序现在在终止子进程后退出1。这就是突破无限主循环的原因