[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: SIGINT handling during async functions
From: |
Tycho Kirchner |
Subject: |
Re: SIGINT handling during async functions |
Date: |
Sat, 21 Jan 2023 13:55:27 +0100 |
Am 16.01.23 um 18:26 schrieb Chet Ramey:
The fix is to add enough state machinery to detect this situation and
behave in a way that can satisfy both the standard and the later
interpretation, while being careful not to undo this work later. This is
obviously not how bash worked in the past.
Thanks for the explanation. While editing the state machinery I would like to
suggest to add a new shopt, let's call it keepsigint, which a user may set to
preserve the SIGINT trap set in the parent shell for all asynchronous commands.
While the POSIX behavior to ignore SIGINT for background processes if job
control is disabled makes totally sense for interactive shells, for scripts to
me it often appears not constructive. Please consider a script launching
several commands in background and waiting for their completion:
cmd1 &
cmd2 &
wait
If the user having launched this script from the interactive terminal aborts it
by hitting Ctrl+C, by default, the shell sends SIGINT to the process group
(pgid) of the script. However, while cmd1 and cmd2 get their signal, they
usually (if they don't override it) ignore it due to above POSIX requirement.
In my experience, what the user usually wants in such a case is to abort cmd1,
cmd2 as well as the script having launched them.
Of course there are ways to kill cmd1 and cmd2 (and possible grandchildren)
explicitly, e.g. by sending an additional TERM signal to the process group,
e.g. (at the top of the script)
trap 'trap "" TERM; env kill -TERM -- -$$; exit 130' INT
However, this is usually only safe, when we are the process group leader
(otherwise we might kill our parent as well!), so we need an additional
[ $$ -eq $(($(ps -o pgid= -p "$$"))) ] || exec setsid --wait "${BASH_SOURCE[0]}"
"$@"
at the top of our script to create a new process group if necessary. Further, applications may
react differently on TERM and INT, making the "signal conversion" undesirable in the
general case. Finally, asynchronously running bash scripts may print "Terminated"
messages which are usually not of interest for a user having aborted the command manually.
Another option would be to enable jobcontrol within the script and kill the
commands that way, e.g.
set -m; cmd1 & jobs=($(jobs -p)); env kill -INT -- "${jobs[@]/#/-}"
However, jobcontrol disables the possibility to suspend the "whole script" with
Ctrl+Z and bears the risk to eventually loose some jobs while without jobcontrol, killing
the single pgid kills all leftovers with high certainty.
A third way is to launch cmd1 and cmd2 with
env --default-signal=SIGINT,SIGQUIT cmd1 &
so they do not ignore SIGINT. That's fine, but has to be repeated for every
command. Further, process substitutions and functions cannot be called that way.
A fourth way is to explicitly set the INT trap within an async command group
before executing the command, like
{ trap 'true' INT; exec cmd1; } &
Personally I regularly use below __async__ function for async commands, command groups,
process substitutions and functions and I'm fine with that. But all these four options
require some typing (and reading) overhead and just don't feel "sane". I think,
bash would really benefit from a 'keepsigint' option. What are your thoughts about that?
Thanks and kind regards
Tycho
_____________________________________________________________________________
__async__ bash -c 'echo first; trap -p; sleep 6'; wait
{ __async__; exec bash -c 'echo second; trap -p; sleep 6'; } & wait
foofunc(){ bash -c 'echo foofunc; trap -p; sleep 6'; }; __async__ foofunc; wait
cat <(__async__; exec bash -c 'echo psub; trap -p; sleep 6';)
__async__(){
local int_trap
int_trap="$(trap -p INT)"
[ -z "$int_trap" ] && int_trap="trap -- 'exit 130' SIGINT"
if [ "${#@}" -eq 0 ]; then
# Already running async, just set parent's INT handler.
eval "$int_trap";
return
fi
if [[ $(type -t "$1") == file ]]; then
# exec into external file so pid is same as if called like 'cmd &'
{ eval "$int_trap"; exec "$@"; } &
else
{ eval "$int_trap"; "$@"; } &
fi
}