0

I have a program that ignores SIGINT but that I want to run in the foreground. I would like to find a way to force it to close on Ctrl-C. Is there any way to write a wrapper (that you'd call ./wrapper.sh my_program) that would force the misbehaving program to quit, potentially by detecting the ignored SIGINT and generating a SIGKILL?

This answer is the exact opposite of what I am looking for -- I would like to force a program that ignores a signal to quit on SIGINT.

3
  • 2
    "by detecting the ignored SIGINT and generating a SIGKILL" -- Do you want the wrapper to actually confirm the SIGINT was ignored? or to assume it was ignored? Commented May 28, 2021 at 8:37
  • 1
    Be careful. Programs that ignore SIGINT usually do it for a very good reason. For kicks, start e.g. vi(1) in a terminal and send it a SIGKILL. A fun puzzle is to restore the terminal to usuability. Commented May 28, 2021 at 11:20
  • @KamilMaciorowski It's fine if it assumes that SIGINT was ignored. Commented May 28, 2021 at 13:45

1 Answer 1

1

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.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.