Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can bash script do the equivalent of Ctrl-C to a background task?

Tags:

bash

Is there any way to invoke a subprocess so that it and all its descendants are sent an interrupt, just as if you Ctrl-C a foreground task? I’m trying to kill a launcher script that invokes a long-running child. I’ve tried kill -SIGINT $child (which doesn’t send the interrupt to its descendants so is a no-op) and kill -SIGINT -$child (which works when invoked interactively but not when running in a script).

Here’s a test script. The long-running script is test.sh --child. When you call test.sh --parent, it invokes test.sh --child & and then tries to kill it. How can I make the parent kill the child successfully?

#!/bin/bash

if [ "$1" = "--child" ]; then
sleep 1000

elif [ "$1" = "--parent" ]; then
"$0" --child &
for child in $(jobs -p); do
  echo kill -SIGINT "-$child" && kill -SIGINT "-$child"
done
wait $(jobs -p)

else
echo "Must be invoked with --child or --parent."
fi

I know that you can modify the long-running child to trap signals, send them to its subprocess, and then wait (from Bash script kill background (grand)children on Ctrl+C), but is there any way without modifying the child script?

like image 591
yonran Avatar asked Feb 04 '13 21:02

yonran


2 Answers

For anyone wondering, this is how you launch childs in the background and kill them on ctrl+c:

#!/usr/bin/env bash
command1 &
pid[0]=$!
command2 &
pid[1]=$!
trap "kill ${pid[0]} ${pid[1]}; exit 1" INT
wait
like image 190
vvo Avatar answered Oct 07 '22 09:10

vvo


You can keep using SIGINT with background tasks with an easy little twist: Put your asynchronous subprocess call in a function or { }, and give it setsid so it has its own process group.

Here's your script keep it's whole first intention:

  • using and propagating SIGINT and not using another signal

  • modifying only the calling from: "$0" --child & to { setsid "$0" --child; } &

  • adding the code necessary to get the PID of your child instance, which is the only process in the background subshell.

Here's your code:

#!/bin/bash

if [ "$1" = "--child" ]; then
sleep 1000

elif [ "$1" = "--parent" ]; then
{ setsid "$0" --child; } &
subshell_pid=$!
pids=$(ps -ax -o ppid,pid --no-headers |
    sed -r 's/^ +//g;s/ +/ /g' |
    grep "^$subshell_pid " | cut -f 2 -d " ");
for child in $pids;  do
  echo kill -SIGINT "-$child" && kill -SIGINT "-$child"
done
wait $subshell_pid

else
echo "Must be invoked with --child or --parent."

Here's the important doc part from bash manual

Process group id effect on background process (in Job Control section of doc):

[...] processes whose process group ID is equal to the current terminal process group ID [..] receive keyboard-generated signals such as SIGINT. These processes are said to be in the foreground. Background processes are those whose process group ID differs from the terminal's; such processes are immune to keyboard-generated signals.

Default handler for SIGINT and SIGQUIT (in Signals section of doc):

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.

and about modification of traps (in trap builtin doc):

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

like image 41
vaab Avatar answered Oct 07 '22 07:10

vaab