1

I'm writing shell application with C and encountered a problem that sending SIGINT to process running script won't stop it.

Sending the same signal to a normal executable works just fine.

Example:

Bash script which just imitates long working script (work.sh):

#! /bin/bash

COUNTER=0
while true
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

C code:

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        sleep(5);
        kill(pid, SIGINT);
    }

    return 0;
}

After sending the signal main process ends but script proceeds printing.

Is signal handling in scripts are somehow different? How you need to stop child process ignoring what is running?

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Faustas Butkus
  • 291
  • 1
  • 5
  • 14
  • 2
    In my experience, Bash does some weird stuff to protect child processes from signals when it shouldn't (or, at least, I'd rather it didn't) — but there's probably documentation somewhere that says "it's supposed to be like that". One possibility is that you don't have permission any more — check the return value from `kill()` and if it's not zero, you know there's a problem (use `errno`, `strerror()`, ``, ``). – Jonathan Leffler Oct 20 '19 at 13:59
  • Note that there's no need to check the return value from `execvp()` — if it returns, it failed. There's also no benefit to using `execvp()` compared with `execv()` when the script you're executing includes a slash `/` in the name; the path search is suppressed by the appearance of the slash. – Jonathan Leffler Oct 20 '19 at 14:01
  • @JonathanLeffler tried checking kill return status and errno. Both are 0. – Faustas Butkus Oct 20 '19 at 14:11
  • 1
    OK; then you need to check what's happening in the shell script. Maybe it is, for some reason, ignoring interrupts. You might find that writing `trap` (on its own; no arguments) in the script reveals that signal 2 or SIGINT is ignored. More likely you won't get that information. You can then try writing an executable that reports on which signals are in default state and which are ignored. That still may not help; it's possible that Bash ignores the signal itself while running other commands and reinstates interrupt handling while it isn't — that seems to be more or less what I've seen. – Jonathan Leffler Oct 20 '19 at 14:17
  • 1
    And that means you may need to use SIGTERM or SIGHUP to terminate the shell and the processes it is running. I've had run-ins with Bash in this area in the past, and I find it infuriating when a script I interrupt keeps going because although the child process terminates as expected, Bash seems to have no qualms about not committing suicide when requested to do so. And, if it is Bash being … uncooperative, shall we say … then there's probably not a lot that can be done except switch to a better shell — but I'm not sure which shell constitutes a "better shell". Korn? Zsh? … I'm not sure. – Jonathan Leffler Oct 20 '19 at 14:18
  • On Macs running macOS Catalina 10.15, the default shell has been changed from Bash to Zsh. I think that's partly that's because of licensing issues; the Bash in use is still a Bash 3.x (with a GPL v2 license), not Bash 4.x or 5.x (with GPL v3). So, maybe Zsh is the direction to go; I'm not sure. – Jonathan Leffler Oct 20 '19 at 14:22

4 Answers4

5

A SOLUTION:

Add this line on top of your work.sh script trap exit SIGINT to have an explicit SIGINT handler:

#! /bin/bash

trap exit SIGINT

COUNTER=0
while true
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

Running work executable now prints:

#1 Working...
#2 Working...
#3 Working...
#4 Working...
#5 Working...

after which it returns back to shell.

THE PROBLEM:

I found this webpage linked in a comment to this question on Unix stackexchange (For the sake of completeness, here also the webpage linked in the accepted answer.) Here's a quote that might explain what's going on:

bash is among a few shells that implement a wait and cooperative exit approach at handling SIGINT/SIGQUIT delivery. When interpreting a script, upon receiving a SIGINT, it doesn't exit straight away but instead waits for the currently running command to return and only exits (by killing itself with SIGINT) if that command was also killed by that SIGINT. The idea is that if your script calls vi for instance, and you press Ctrl+C within vi to cancel an action, that should not be considered as a request to abort the script.

So imagine you're writing a script and that script exits normally upon receiving SIGINT. That means that if that script is invoked from another bash script, Ctrl-C will no longer interrupt that other script.

This kind of problem can be seen with actual commands that do exit normally upon SIGINT by design.

EDIT:

I found another Unix stackexchange answer that explains it even better. If you look at bash(1) man pages the following is also quite explanatory:

Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.

especially when considering that:

Signals ignored upon entry to the shell cannot be trapped, reset or listed.

Basically, running work.sh runs it in a separate execution environment:

When a simple command other than a builtin or shell function is to be executed, it is invoked in a separate execution environment.

This includes the signal handlers which (if not explicitly present) will ignore SIGINT and SIGQUIT by default.

Community
  • 1
  • 1
gstukelj
  • 2,291
  • 1
  • 7
  • 20
2

After trying your example, I came to these conclusions.

Even when run interactively in a terminal, the work.sh script does not react to SIGINT sent from another terminal; but it does when interrupted with [Ctrl][c] from its own terminal.
I guess there is some kind of magic from bash in the terminal management...

Adding an explicit management of SIGINT in your script seems to make it actually react to SIGINT (whatever the origin, even from your C code).

#! /bin/bash

must_stop=""
function sigint_handler()
{
    echo "SIGINT"
    must_stop="1"
}
trap sigint_handler SIGINT

COUNTER=0
while [ -z "${must_stop}" ]
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

As stated in the comments, if the only purpose is to stop this script, may be SIGHUP or SIGTERM should be considered; SIGINT is generally more related to interactively hitting [Ctrl][c] in the terminal than to explicitly sending a signal from a program.

Note also that capturing the signal with trap offers the ability to exit cleanly from the loop rather than violently in the middle of any instruction.

prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • No need to feel remotely ashamed — my comments are an admission that I've run into similar problems but not worked out how to resolve them. I'm happy to have your help (confirmation that there is an issue). Does using `trap "kill -2 $$" 2` help? I'm being supremely lazy here, too. – Jonathan Leffler Oct 20 '19 at 15:26
  • @JonathanLeffler Thanks for your words. I tried with `kill -2 $$` and it did nothing (not even an infinite recursion ;^) but re-emitting another signal, `kill -1 $$` for example, works. – prog-fh Oct 20 '19 at 15:34
  • Using HUP or TERM (not KILL if you can help it) may be the best that can be done. Think about whether it's worth adding some information to your answer — I'm not sure whether it warrants an addition. I'd like to get to a point where the script stops when interrupted, as well as the programs that it is running. I'm pretty sure classic shells did that — I only started to run into this problem when I switched to Bash (mostly — I still use Korn shell on some machines, and I've not used Zsh seriousy at all). – Jonathan Leffler Oct 20 '19 at 15:39
1

Allow me to point out that the shutdown approach could be improved, since SIGINT is suboptimal when used alone and doesn't guarantee a shutdown.

In fact, SIGINT shouldn't be used as an exit signal.

SIGINT could be used to notify the child process of a soft shutdown (for example, when the running process is a server script).

This will allow the child process to shutdown gracefully.

A hard shutdown requires SIGKILL, which is a signal that cannot be ignored and which is enforced by the OS.

The C code could, for example, look like this:

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        int result = 0;
        sleep(5);
        kill(pid, SIGINT); /* soft shutdown */
        sleep(1);
        if(waitpid(pid, &result, WNOHANG) == -1) {
           /* we didn't exit? perform a hard shutdown */
           sleep(4);
           kill(pid, SIGKILL);
        }
    }
    return 0;
}

This will notify the child process of both a soft and a hard shutdown, allowing time for both.

...

Of course, a better approach might use sigtimedwait.

Myst
  • 18,516
  • 2
  • 45
  • 67
0

You could also:

  1. Change kill(pid, SIGINT); to killpg(getpid(), SIGINT); this is what pressing ctrl-c would do; but it will hit your main() as well.
  2. If you don’t want your main hit, do a setpgid(0,0) just before the execvp(), and change the kill(pid, SIGINT) to killpg(pid, SIGINT).

Then the shell script doesn’t have to “help”. To be a little more overt, leave your shell script alone, and change your program to:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        setpgid(0,0);
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        sleep(5);
        killpg(pid, SIGINT);
    }

    return 0;
}
mevets
  • 10,070
  • 1
  • 21
  • 33