Catching Control-C and a gotcha with shell scripts
Suppose, not entirely hypothetically, that you have some sort of
spiffy program that wants to use Control-C
as a key binding to get it to take some action. In Unix, there are
two ways of catching Control-C for this sort of thing. First, you
can put the terminal into raw mode, where Control-C
becomes just another character that you read from the terminal and
you can react to it in any way you like. This is very general but
it has various drawbacks, like you have to manage the terminal state
and you have to be actively reading from the terminal so you can
notice when the key is typed. The simpler alternative way of catching
Control-C is to set a signal handler for SIGINT
and then react
when it's invoked. With a signal handler, the kernel's standard
tty input handling does all of that hard
work for you and you just get the end result in the form of an
asynchronous SIGINT
signal. It's quite convenient and leaves you
with a lot less code and complexity in your spiffy Control-C catching
program.
Then some day you run your spiffy program from inside a shell script
(perhaps you wanted to add some locking), hit Control-C to signal your
program, and suddenly you have a mess (what sort of a mess depends
on whether or not your shell does job control). The problem is that
when you let the kernel handle Control-C by delivering a SIGINT
signal, it doesn't just deliver it to your program; it delivers it
to the shell script and in fact any other programs that the shell
script is also running (such as a flock
command used to add
locking). The shell script and these other programs are not expecting
to receive SIGINT
signals and haven't set up anything special to
handle it, so they will get killed.
(Specifically, the kernel will send the SIGINT
to all processes
in the foreground process group.)
Since your shell was running the shell script as your command and the shell script exited, many shells will decide that your command has finished. This means they'll show you the shell prompt and start interacting with you again. This can leave your spiffy program and your shell fighting over terminal output and perhaps terminal input as well. Even if your shell and your spiffy program don't fight for input and write their output and shell prompt all over each other, generally things don't go well; for example, the rest of your shell script isn't getting run, because the shell script died.
Unfortunately there isn't a good general way around this problem.
If you can arrange it, the ideal is for the wrapper shell script
to wind up directly exec
'ing your spiffy program so there's nothing
else a SIGINT
will be sent to (and kill). Failing that, you might
have to make the wrapper script trap and ignore SIGINT
while it's
running your program (and to make your program unconditionally
install its SIGINT
signal handler, even if SIGINT
is ignored
when the program starts).
Speaking from painful personal experience, this is an easy issue
to overlook (and a mysterious one to diagnose). And of course
everything works when you test your spiffy program by running it
directly, because then the only process getting a SIGINT
is the
one that's prepared for it.
|
|