help-bash
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Interrupting scripts with asynchronous subprocesses


From: Yuri Kanivetsky
Subject: Re: Interrupting scripts with asynchronous subprocesses
Date: Sun, 14 Jul 2024 07:58:16 +0300

> There was a discussion about this after bash-5.2 came out:
>
> https://lists.gnu.org/archive/html/bug-bash/2023-01/msg00044.html
>
> that resulted in some behavior changes.

Okay, I've read the discussion and experimented with the examples. Let
me try to explain what's going on there with my own words. But let me
start with a couple of cases of my own first:

d.sh:

sleep infinity
echo after sleep

$ ps -eHo pid,ignored,args --signames
...
 428564 QUIT ./bash d.sh
 428565 -      sleep infinity

$ bash d.sh
^C

When one presses Ctrl-C the foreground process group (bash and sleep)
receives SIGINT. sleep has SIG_DFL and dies. bash notices that sleep
has died from SIGINT:

https://git.savannah.gnu.org/cgit/bash.git/tree/jobs.c?h=bash-5.2#n4101

and terminates itself:

https://git.savannah.gnu.org/cgit/bash.git/tree/jobs.c?h=bash-5.2#n4141

I guess bash tries to follow the WCE approach described here:

https://www.cons.org/cracauer/sigint.html

--

e.sh:

{ sleep infinity
echo after sleep; } &
wait

$ ps -eHo pid,ignored,args --signames
...
 428568 QUIT     ./bash e.sh
 428569 INT,QUIT   ./bash e.sh
 428570 -            sleep infinity

$ bash e.sh
^Cafter sleep

Now { ... } gets SIG_IGN as an async process and only sleep (of those
2) receives SIGINT. sleep dies, but now the condition mentioned above:

https://git.savannah.gnu.org/cgit/bash.git/tree/jobs.c?h=bash-5.2#n4101

is not met (wait_sigint_received == 0 since { ... } has SIG_IGN), bash
goes on and prints "after sleep."

In other words { ... } has SIG_IGN, so it doesn't terminate on SIGINT.

--

f.sh:

t='echo INT ${FUNCNAME[0]-main} >&2'
trap "$t" INT
foofunc(){ sleep 3; echo foo >&2; }
foofunc &
sleep 5

$ ps -eHo pid,ignored,args --signames
...
 428573 QUIT ./bash f.sh
 428574 QUIT   ./bash f.sh
 428575 -        sleep 3
 428576 -      sleep 5

$ bash f.sh
^CINT main

Async builtins and functions set SIGINT to SIG_IGN (as async processes
usually do):

https://git.savannah.gnu.org/cgit/bash.git/tree/execute_cmd.c?h=bash-5.2#n5554

but with builtins and functions it's soon changed to termsig_sighandler():

https://git.savannah.gnu.org/cgit/bash.git/tree/execute_cmd.c?h=bash-5.2#n5365

The outer bash has a SIGINT handler which prints "INT main." The sleep
processes has SIG_DFL so they die. foofunc() sees that its child has
died from SIGINT and dies itself.

--

g.sh:

t='echo INT ${FUNCNAME[0]-main} >&2'
trap "$t" INT
foofunc(){ trap "$t" INT; sleep 3; echo foo >&2; }
foofunc &
sleep 5

$ ps -eHo pid,ignored,args --signames
...
 428579 QUIT ./bash g.sh
 428580 QUIT   ./bash g.sh
 428582 -        sleep 3
 428581 -      sleep 5

$ bash g.sh
^CINT foofunc
foo
INT main

Now both bash processes have SIGINT handler, and they both print "INT
...." The reason foo is printed is because foofunc()'s SIGINT handler
doesn't terminate the process.

--

h.sh:

t='echo INT ${FUNCNAME[0]-main} >&2'
trap "$t" INT
foofunc(){ sleep 3; echo foo >&2; }
{ foofunc; } &
sleep 5

$ ps -eHo pid,ignored,args --signames
...
 428585 QUIT     ./bash h.sh
 428586 INT,QUIT   ./bash h.sh
 428588 -            sleep 3
 428587 -          sleep 5

$ bash h.sh
^CINT main
foo

The outer bash prints its "INT main." That much is clear. { foofunc; }
has SIG_IGN because it's an async shell. So on SIGINT it doesn't
terminate (see e.sh), and prints foo.

--

i.sh:

t='echo INT ${FUNCNAME[0]-main} >&2'
trap "$t" INT
foofunc(){ trap -p; sleep 3; echo foo >&2; }
{ foofunc; } &
sleep 5

$ ps -eHo pid,ignored,args --signames
...
 428591 QUIT     ./bash i.sh
 428592 INT,QUIT   ./bash i.sh
 428594 INT          sleep 3
 428593 -          sleep 5

$ bash i.sh
trap -- '' SIGINT
^CINT main
foo

The tricky part here is that now { foofunc; } and its child both have
SIG_IGN. As such they both ignore SIGINT and finish as if nothing
happened (foo is printed after a delay).

Now why do they both have SIG_IGN? Normally when { foofunc; } invokes
sleep the latter inherits the original handlers (the handlers that {
foofunc; } inherited from its parent). In both cases (h.sh, i.sh) that
is SIG_DFL. But `trap -p` changes the original handler:

https://git.savannah.gnu.org/cgit/bash.git/tree/builtins/trap.def?h=bash-5.2#n134
https://git.savannah.gnu.org/cgit/bash.git/tree/sig.c?h=bash-5.2#n272
https://git.savannah.gnu.org/cgit/bash.git/tree/trap.c?h=bash-5.2#n1537

As such with i.sh sleep inherits SIG_IGN.

--

j.sh:

./bash -c 'echo first; trap -p' & wait
{ ./bash -c 'echo second; trap -p'; } & wait
{ trap -p >/dev/null; ./bash -c 'echo third; trap -p'; } & wait

$ bash j.sh
first
trap -- '' SIGINT
trap -- '' SIGQUIT
second
third
trap -- '' SIGINT

In the first case:

$ ps -eHo pid,ignored,args --signames
...
 428609 QUIT     ./bash j.sh
 428610 INT,QUIT   ./bash -c echo first; trap -p

`bash -c` gets SIG_IGN as an async process.

In the second case:

$ ps -eHo pid,ignored,args --signames
...
 428616 QUIT     ./bash j.sh
 428618 INT,QUIT   ./bash j.sh
 428619 QUIT         ./bash -c echo second; trap -p

{ ... } gets SIG_IGN, but it doesn't get inherited by `./bash -c ...`
because { ... } inherited SIG_DFL from its parent (case h.sh).

In the third case:

$ ps -eHo pid,ignored,args --signames
...
 428623 QUIT     ./bash j.sh
 428627 INT,QUIT   ./bash j.sh
 428628 INT,QUIT     ./bash -c echo third; trap -p

`trap -p >/dev/null` makes SIG_IGN inherited by `bash -c` (case i.sh).

--

The key takeaways:

* It's pretty much messed up^W^W^W^W^W :) (Just in case, I don't mean
to offend anyone here. It's meant as a joke.)

* Async processes generally get SIG_IGN:

"If job control is disabled (see the description of set -m) when the
shell executes an asynchronous list, the commands in the list shall
inherit from the shell a signal action of ignored (SIG_IGN) for the
SIGINT and SIGQUIT signals."

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_11

E.g. { ... } in `bash -c '{ ... } & wait'`, `bash a.sh` in `bash -c
'bash a.sh & wait'`.

But not async functions or builtins. E.g. f in `bash -c 'f() { ... };
f & wait'`.

* SIG_IGN is inherited by children of a "new" shell, such as `bash
a.sh` in `bash -c 'bash a.sh & wait'` (but not { ... } in `bash -c '{
... } & wait'`).

Also `trap -p` makes SIG_IGN inherited. E.g. `sleep infinity` in `bash
-c '{ trap -p; sleep infinity; } & wait'` inherits SIG_IGN.

* Generally async shells can't override or reset SIGINT:

"Signals that were ignored on entry to a non-interactive shell cannot
be trapped or reset, although no error need be reported when
attempting to do so."

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_28

E.g. `bash a.sh` in `bash -c 'bash a.sh & wait'`.

But non-"new" shells can:

https://www.austingroupbugs.net/view.php?id=751

E.g. { ... } in `bash -c '{ ... } & wait'`.

--

That is of course my understanding. Correct me where I'm wrong. Also
maybe you have anything to add. In some cases I don't have a better
explanation than "because that's how it is in the code." Maybe you can
explain why something is the way it is.

Regards,
Yuri



reply via email to

[Prev in Thread] Current Thread [Next in Thread]