Let's build a wrapper that "converts" SIGINT into SIGKILL.
We need a process that runs along my_program
and gets SIGINT upon Ctrl+c. When it receives SIGINT it should send SIGKILL to my_program
. Note we don't need my_program
to actually receive SIGINT caused by Ctrl+c; it's enough it gets SIGKILL caused by the extra process.
Relevant facts:
- A standard way to run one process along another is to run one of them asynchronously in the background (i.e. with terminating
&
).
- When you hit Ctrl+c, it's your terminal emulator who sends SIGINT to processes in the foreground process group.
- Some (simple) shells run everything in the same process group. If they are told to run a command in the background then they will redirect its stdin to
/dev/null
or equivalent file to prevent the command from stealing input.
- Other shells can run each command in a separate process group. If they are told to run a command in the background then they will leave its stdin as-is; still the command in the background will not be able to steal input from the controlling terminal because of
SIGTTIN
. This allows the shell to move a job from the background to the foreground by informing the terminal of the new foreground process group. The mechanism is called job control and can be disabled. In scripts it's disabled by default.
- Either way a process in the background cannot read from the terminal. This means we shouldn't run
my_program
in the background in case the stdin is the terminal and my_program
needs to read from it.
- Unfortunately in some shells we shouldn't run the other process in the background either. Some shells use separate process groups even when job control is disabled. The other process not being in the foreground process group will not receive SIGINT upon Ctrl+c, so it won't be able to "convert" it to SIGKILL.
- In my Debian 10
posh
is a shell that runs everything in the same process group.
This leads to the following wrapper:
#!/usr/bin/env posh
( trap 'kill -s KILL 0' INT
while kill -s 0 "$$" 2>/dev/null; do sleep 1; done
) &
exec "$@"
exec "$@"
runs my_program
(or whatever you specify), possibly with arguments. Thanks to exec
my_program
will replace the wrapper. Not only it will be able to read from the stdin which may be a terminal; its PID will be the one of the replaced wrapper. In this sense the wrapper is transparent. Additionally this has a nice feature: the PID of my_program
is known before my_program
starts and we can easily use it in the other process (in this case in a subshell in the background) to detect when my_program
actually terminates.
The trap
"converts" SIGINT into SIGKILL. Note kill -s KILL 0
sends SIGKILL to the entire process group, including children of my_program
if they are in the group (if you want to kill my_program
only then use kill -s KILL "$$"
instead). Nevertheless kill -s 0 "$$"
tests the existence of my_program
only.
There's an alternative that does not require a shell that runs everything in the same process group. The trick is: processes in a pipeline should run in one process group; and by clever redirecting you can build a pipeline where pieces are not connected to one another.
#!/bin/sh -
exec 9>&1
( "$@"; kill -s TERM 0 ) >&9 9>&- | (
trap 'kill -s KILL 0' INT
while :; do sleep 1; done
)
In this variant my_program
will not replace the wrapper. The second kill
is to "convert" SIGINT to SIGKILL. The first kill
is to terminate the loop in case my_program
exits in circumstances when the loop would otherwise survive.
There are few scenarios that may not work as you expect (with any wrapper). Among them:
- If
my_program
forks and exit, leaving the real job to the child. The troublesome program in question most likely does not behave like this, since you tried to Ctrl+c it. But in general it might.
- If
my_program
spawns child processes in another process group(s) and you'd like to kill them along with their parent.
- If
my_program
configures the terminal not to send SIGINT upon Ctrl+c. If you suspect this happens then improve the wrapper by putting stty -F /dev/tty intr ^C
between sleep 1
and done
. In this case the program may even not ignore SIGINT, it may simply make sure it won't get it from the terminal. So maybe SIGKILL is overkill; maybe it's enough to restore this functionality of the terminal and Ctrl+c will start working.
From a comment:
Programs that ignore SIGINT usually do it for a very good reason.
True. Try SIGTERM or SIGHUP instead of SIGKILL. Maybe the program in question does not ignore at least one of these and exits gracefully with proper cleanup.
vi(1)
in a terminal and send it a SIGKILL. A fun puzzle is to restore the terminal to usuability.