Hermann Speiche
Hermann Speiche

Reputation: 924

Bash: Why does parent script not terminate on SIGINT when child script traps SIGINT?

script1.sh:

 #!/bin/bash    

./script2.sh
 echo after-script

script2.sh:

#!/bin/bash

function handler {
  exit 130
}

trap handler SIGINT

while true; do true; done

When I start script1.sh from a terminal, and then use Ctrl+C to send SIGINT to its process group, the signal is trapped by script2.sh and when script2.sh terminates, script1.sh prints "after-script". However, I would have expected script1.sh to immediately terminate after the line that invokes script2.sh. Why is this not the case in this example?

Additional remarks (edit):

Upvotes: 15

Views: 4135

Answers (5)

Gregg Wonderly
Gregg Wonderly

Reputation: 29

The correct way this should work is through setpgrp(). All children of shell should be placed in the same pgrp. When SIGINT is signaled by the tty driver, it will be summarily delivered to all processes. The shell at any level should note the receipt of the signal, wait for children to exit and then kill themselves, again, with no signal handler, with sigint, so that their exit code is correct.

Additionally, when SIGINT is set to ignore at startup by their parent process, they should ignore SIGINT.

A shell should not "check if a child exited with sigint" as any part of the logic. The shell should always just honor the signal it receives directly as the reason to act and then exit.

Back in the day of real UNIX, SIGINT stopped the shell and all sub processes with a single key stroke. There was never any problem with the exit of a shell and child processes continuing to run, unless they themselves had set SIGINT to ignore.

For any shell pipeline, their should be a child process relationship created from pipelines going right to left. The right most command is the immediate child of the shell since thats the last process to exit normally. Each command line before that, is a child of the process immediately to the right of the next pipe symbol or && or || symbol. There are obvious groups of children around && and || which fall out naturally.

in the end, process groups keep things clean so that nohup works as well as all children receiving SIGINT or SIGQUIT or SIGHUP or other tty driver signals.

Upvotes: 0

jim-minter
jim-minter

Reputation: 431

The second part of @seanmcl's updated answer is correct and the link to http://www.cons.org/cracauer/sigint.html is a really good one to read through carefully.

From that link, "You cannot 'fake' the proper exit status by an exit(3) with a special numeric value, even if you look up the numeric value for your system". In fact, that's what is being attempted in @Hermann Speiche's script2.sh.

One answer is to modify function handler in script2.sh as follows:

function handler {
  # ... do stuff ...
  trap INT
  kill -2 $$
}

This effectively removes the signal handler and "rethrows" the SIGINT, causing the bash process to exit with the appropriate flags such that its parent bash process then correctly handles the SIGINT that was originally sent to it. This way, using set -e or any other hack is not actually required.

It's also worth noting that if you have an executable that behaves incorrectly when sent a SIGINT (it doesn't conform to "How to be a proper program" in the above link, e.g. it exits with a normal return-code), one way of working around this is to wrap the call to that process with a script like the following:

#!/bin/bash

function handler {
  trap INT
  kill -2 $$
}

trap handler INT
badprocess "$@"

Upvotes: 2

seanmcl
seanmcl

Reputation: 9946

New answer:

This question is far more interesting than I originally suspected. The answer is essentially given here:

What happens to a SIGINT (^C) when sent to a perl script containing children?

Here's the relevant tidbit. I realize you're not using Perl, but I assume Bash is using C's convention.

Perl’s builtin system function works just like the C system(3) function from the standard C library as far as signals are concerned. If you are using Perl’s version of system() or pipe open or backticks, then the parent — the one calling system rather than the one called by it — will IGNORE any SIGINT and SIGQUIT while the children are running.

This explanation is the best I've seen about the various choices that can be made. It also says that Bash does the WCE approach. That is, when a parent process receives SIGINT, it waits until its child process returns. If that process handled exited from a SIGINT, it also exits with SIGINT. If the child exited any other way it ignores SIGINT.

There is also a way that the calling shell can tell whether the called program exited on SIGINT and if it ignored SIGINT (or used it for other purposes). As in the WUE way, the shell waits for the child to complete. It figures whether the program was ended on SIGINT and if so, it discontinue the script. If the program did any other exit, the script will be continued. I will call the way of doing things the "WCE" (for "wait and cooperative exit") for the rest of this document.

I can't find a reference to this in the Bash man page, but I'll keep looking in the info docs. But I'm 99% confident this is the correct answer.

Old answer:

A nonzero exit status from a command in a Bash script does not terminate the program. If you do an echo $? after ./script2.sh it will show 130. You can terminate the script by using set -e as phs suggests.

$ help set
...
-e  Exit immediately if a command exits with a non-zero status.

Upvotes: 14

konsolebox
konsolebox

Reputation: 75548

You can also let your second script send a terminating signal on its parent script by SIGHUP, or other safe and usable signals like SIGQUIT in which the parent script may consider or trap as well (sending SIGINT doesn't work).

script1.sh:

#!/bin/bash

trap 'exit 0' SIQUIT  ## We could also just accept SIGHUP if we like without traps but that sends a message to the screen.

./script2.sh  ## or "bash script.sh" or "( . ./script.sh; ) which would run it on another process
echo after-script

script2.sh:

#!/bin/bash

SLEEPPID=''

PID=$BASHPID
read PPID_ < <(exec ps -p "$PID" -o "$ppid=")

function handler {
  [[ -n $SLEEPPID ]] && kill -s SIGTERM "$SLEEPPID" &>/dev/null
  kill -s SIGQUIT "$PPID_"
  exit 130
}

trap handler SIGINT

# better do some sleeping:

for (( ;; )); do
  [[ -n $SLEEPPID ]] && kill -s 0 "$SLEEPPID" &>/dev/null || {
    sleep 20 &
    SLEEPPID=$!
  }
  wait
done

Your original last line in your script1.sh could have just like this as well depending on your scripts intended implementation.

./script2.sh || exit
...

Or

./script2.sh
[[ $? -eq 130 ]] && exit
...

Upvotes: 0

devnull
devnull

Reputation: 123608

The reason is your script1.sh doesn't terminate is that script2.sh is running in a subshell. To make the former script exit, you can either set -e as suggested by phs and seanmcl or force the script2.sh to run in the same shell by saying:

. ./script2.sh

in your first script. What you're observing would be apparent if you were to do set -x before executing your script. help set tells:

  -x  Print commands and their arguments as they are executed.

Upvotes: 0

Related Questions