6

If I have a Bash script like:

function repeat {
    while :; do
        echo repeating; sleep 1
    done
}
repeat &
echo running once

running once is printed once but repeat's fork lives forever, printing endlessly.

How should I prevent repeat from continuing to run after the script which created it has exited?

I thought maybe explicitly instantiating a new bash -c interpreter would force it to exit as its parent has disappeared, but I guess orphaned processes are adopted by init or PID 1.

Testing this using another file:

# repeat.bash
while :; do echo repeating; sleep 1; done

# fork.bash
bash -c "./repeat.bash & echo an exiting command"

Running ./fork.bash still causes repeat.bash to continue to run in the background forever.

The simple and lazy solution is to add the line to fork.bash:

pkill repeat.bash

But you had better not have another important process with that name, or it will also be obliterated.

  1. I wonder, if there is a better or accepted way to handle background jobs in forked shells that should exit when the script (or process) that created them has exited?

  2. If there is no better way than blindly pkilling all processes with the same name, how should a repeating job that runs alongside something like a webserver be handled to exit? I want to avoid a cron job because the script is in a git repository, and the code should be self-contained without changing system files in /etc/.

2 Answers 2

7

This kills the background process before the script exits:

trap '[ "$pid" ] && kill "$pid"' EXIT

function repeat {
    while :; do
        echo repeating; sleep 1
    done
}
repeat &
pid=$!
echo running once

How it works

  • trap '[ "$pid" ] && kill "$pid"' EXIT

    This creates a trap. Whenever the script is about to exit, the commands in single-quotes will be run. That command checks to see if the shell variable pid has been assigned a non-empty value. If it has, then the process associated with pid is killed.

  • pid=$!

    This saves the process id of the preceding background command (repeat &) in the shell variable pid.

Improvement

As Patrick points out in the comments, there is a chance that the script could be killed after the background process starts but before the pid variable is set. We can handle that case with this code:

my_exit() {
    [ "$racing" ] && pid=$!
    [ "$pid" ] && kill "$pid"
}
trap my_exit EXIT

function repeat {
    while :; do
        echo repeating; sleep 1
    done
}

racing=Y
repeat &
pid=$!
racing=

echo running once
7
  • You can avoid the need for the [ "$pid" ] conditional by just putting the trap after the repeat &. You can also avoid the need for a variable by doing trap "kill $!" EXIT.
    – phemmer
    May 11, 2017 at 4:32
  • 1
    @Patrick You are exactly right but the key advantage to the approach above is that it scales well as the complexity of the script increases. If, for example, the script expands to have more than one function that has to be killed or maybe a temporary file that needs to be deleted or the logical flow becomes more complex, then the approach of a single trap statement at the top with tests like [ "$pid1" ] is easy to maintain and extend.
    – John1024
    May 11, 2017 at 6:08
  • 1
    @Patrick, setting the trap after running the background process runs the risk of the someone killing the script just after the background process is started, but before the trap is set up. Better to avoid race conditions like that.
    – ilkkachu
    May 11, 2017 at 7:36
  • 1
    @ilkkachu Setting the variable after running the background process runs the risk of the someone killing the script just after the background process is started, but before the variable is set up.
    – phemmer
    May 11, 2017 at 12:22
  • 1
    @Patrick, well, that's also true, and probably unavoidable. Except by killing via job id, but I don't know if there's a good way to get the jobid except by counting how many are used.
    – ilkkachu
    May 11, 2017 at 12:26
3

The tcsh shell has a hup command to tell the shell to kill the command upon exit:

hup mycommand...

That doesn't help for running function as tcsh has no support for functions though.

In zsh, when job control is enabled, that's done by default for all background jobs.

$ zsh -c 'set -m; sleep 2 &'; ps
[1] 23672
zsh:1: warning: 1 jobs SIGHUPed

Enabling job control in scripts can have side effects though.

bash can do it as well but only when interactive (though calling it with -i on a script will also do it) and only when a login shell (!?). And you need to enabled the huponexit option. So:

 bash -O huponexit -lic 'sleep 2 &'

That has quite a few side effects though so is probably not a good idea.

Another option with zsh would be to kill all known running children processes (in any case we can't easily know about grand-children (nor that we would always want to kill them)) upon exit. zsh makes that available in the $jobstates associative array:

TRAPEXIT() kill -s HUP ${${jobstates[(R)running:*]/#*:/}/%=*/}
trap exit INT HUP TERM

Other shells don't expose their list of children, but you could kill the processes whose ppid is $$, like:

trap 'pkill -HUP -P "$$"' EXIT INT HUP TERM
1
  • Nice answer covering more than bash -- can you elaborate, the other undesirable side effects of bash -O huponexit -lic?
    – cat
    May 11, 2017 at 18:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .