我需要防止 SIGINT(Ctrl-C)从子 shell 传播到 Zsh 中的父 shell 函数。
这是一个简单的例子:
function sox-record {
local output="${1:-$(mktemp).wav}"
(
rec "${output}" trim 0 300 # Part of sox package
)
echo "${output}" # Need this to continue executing after Ctrl-C
}
function audio-postprocess {
local audio="$(sox-record)"
# Process the audio file...
echo "${audio}"
}
function audio-transcribe {
local audio="$(audio-postprocess)"
# Send to transcription service...
transcribe_audio "${audio}" # Never reached if Ctrl-C during recording
}
当前的解决方法需要在每个级别捕获 SIGINT,这会导致重复且容易出错的代码:
function sox-record {
local output="${1:-$(mktemp).wav}"
setopt localtraps
trap '' INT
(
rec "${output}" trim 0 300
)
trap - INT
echo "${output}"
}
function audio-postprocess {
setopt localtraps
trap '' INT
local audio="$(sox-record)"
trap - INT
# Process the audio file...
echo "${audio}"
}
function audio-transcribe {
setopt localtraps
trap '' INT
local audio="$(audio-postprocess)"
trap - INT
# Send to transcription service...
transcribe_audio "${audio}"
}
当用户按下 Ctrl-C 停止录制时,我想要:1.rec
子进程终止(工作)2. 父函数继续执行(需要在每个调用者中捕获 SIGINT)
我知道:
- SIGINT 发送给前台进程组中的所有进程
- 使用
setsid
会创建一个新的进程组,但会阻止信号到达子进程 - 添加
trap '' INT
父级需要所有调用者也捕获 SIGINT 以防止传播
有没有办法将 SIGINT 隔离到子 shell,而无需在所有父函数中处理信号?或者,由于 Unix 进程组和信号传播的工作方式,这从根本上是不可能的?
我看了一下这个问题,并尝试这样做:
function sox-record {
local output="${1:-$(mktemp).wav}"
zsh -mfc "rec "${output}" trim 0 300" </dev/null >&2 || true
echo "${output}"
}
当我仅调用 时sox-record
,此方法有效,但当我调用父函数(如 )时audio-postprocess
,Ctrl-C 不会执行任何操作。(而且我必须使用pkill
来杀死rec
。)
function audio-postprocess {
local audio="$(sox-record)"
# Process the audio file...
echo "${audio}"
}
SIGINT 不会传播到父进程。一旦启动
^C
,内核就会将 SIGINT 发送到终端的前台进程组。当该 zsh 脚本在终端中启动时,您的交互式 shell 将为其创建一个进程组(作业),使其成为终端的前台进程组并执行该脚本。该脚本启动的所有进程(包括子 shell 和正在执行的进程)都
rec
将位于该组中,并且都将在 时接收 SIGINT^C
。如果您希望只有正在运行的进程
rec
接收该信号,请使用 在顶层全局忽略 SIGINTtrap '' INT
,并仅为 恢复rec
它(trap - INT; rec...)
。您也可以将
trap
's 移到里面audio-transcribe
,但是您需要记住不要在子 shell 中运行它,否则忽略 SIGINT 将仅适用于该子 shell 及其后代,而不是父进程。让脚本本身执行作业控制,即放入
rec
其自己的进程组并使该进程组成为前台进程组是一种也可以起作用的方法,但在 zsh(至少 5.9)中,您不能单独使用选项monitor
(由设置set -m
)来执行此操作,因为在非交互式调用中,它会为命令创建进程组,但不会更改终端的前台进程组,因此它会产生与您想要的相反的效果。为了
zsh
愿意进行终端作业控制,您需要interactive
(-i
)。既不是
-i
也不是-m
,ps
与父进程位于同一进程组,且位于前台。单独使用时
-m
,ps
处于一个新的进程组中,但是该进程组尚未成为终端的前台进程组 (tpgid),因此它实际上处于后台,因此不会收到 SIGINT^C
。其中
-i
,暗示处于一个新的进程组中-m
,ps
该进程组此时位于前台,而zsh
本身则位于后台。然而,在脚本中玩弄作业控制通常是一个坏主意,因为它是各种令人讨厌的意外行为的根源,所以我不会这么做。如果你这么做了,请确保你这样写:
不要在 zsh 代码中嵌入扩展,因为这会使其成为命令注入漏洞。
$output