17

I am fairly new to shell scripting. I have a script similar to the following,

while [ true ]
do
  sleep 5m
  # PART X: code that executes every 5 mins
done

While this is running (1) how can I intercept the program from outside and (2) stop the sleep process and execute PART X right away? Can I use SIGNALS for that purpose? Or is there a better way?

Can you point out a general direction to solving these type of issues?

3
  • 2
    An easy way around is not to sleep for 5 minutes, but to sleep for 5 minutes worth of 1/2/3 second blocks, waking up to check a sleep-exit condition.
    – Kingsley
    Feb 9 '21 at 1:58
  • 2
    I saw this question earlier, and looked back at the title, and thought who in their right mind would be on-call while on a camping vacation. Feb 9 '21 at 6:22
  • 2
    For this kind of thing, it might be worth elevating it to a proper program in Python. An alternative would be to run "X" from cron, but have it "touch" a file to indicate when it last ran so that you can manually run X when required too.
    – pjc50
    Feb 9 '21 at 10:12
21

Handling the timing is the script's responsibility. Even if that means using /bin/sleep today, it might not in the future, so killing that isn't actually guaranteed to work long-term. Well, I guess you can make that guarantee, but it's neater not to. My point is you shouldn't kill the sleep directly from outside the script, since the sleep is an implementation detail. Instead, have your script trap a different signal, like SIGUSR1 and send that to your script.

A simple example might look like

#!/usr/bin/env bash

kill_the_sleeper () {
    # This probably isn't really needed here
    # If we don't kill the sleep process, it'll just
    # hang around in the background until it finishes on its own
    # which isn't much of an issue for "sleep" in particular
    # But cleaning up after ourselves is good practice, so let's
    # Just in case we end up doing something more complicated in future
    if [ -v sleep_pid ]; then
        kill "$sleep_pid"
    fi
}

trap kill_the_sleeper USR1 EXIT

while true; do
    # Signals don't interrupt foreground jobs,
    # but they do interrupt "wait"
    # so we "sleep" as a background job
    sleep 5m &
    # We remember its PID so we can clean it up
    sleep_pid="$!"
    # Wait for the sleep to finish or someone to interrupt us
    wait
    # At this point, the sleep process is dead (either it finished, or we killed it)
    unset sleep_pid

    # PART X: code that executes every 5 mins
done

Then you can cause PART X to run by running kill -USR1 "$pid_of_the_script" or pkill -USR1 -f my_fancy_script

This script isn't perfect by any means, but it should be decent for simple cases at least.

2
  • NICE. I was expecting to get this kind of answer. What do you mean by it isn't perfect? Can it be more cleaner? Feb 9 '21 at 5:59
  • 8
    The script takes care of most, but not all race conditions by setting/unsetting/testing $sleep_pid. Sending USR1 while the shell is just starting the sleep 5m, or twice in rapid succession while the wait is active, may break some of the logic. That's why it isn't perfect, but this level of perfect is very hard to achieve in the shell alone, and just a little bit easier in C. Feb 9 '21 at 9:12
13

In another terminal,
Kill the sleep process:

pkill -f "sleep 5m"

The loop will go on.


If pkill is not available in your OS, you can get the PID using:

$ ps aux | grep '[s]leep'
username 14628  0.0  0.0   8816   672 pts/19   S+   08:33   0:00 sleep 5m

Then run kill PID to kill the process (here: kill 14628).


This is good for manually killing your sleep, but it might not be a good solution for general purposes in scripting, as it will kill all processes with sleep 5m anywhere in the command.


Safer alternative if you can edit the script:

Change your script to write the PID to a file:

TMP_DIR="$HOME/.cache/myscript/"
mkdir -p "$TMP_DIR"
while [ true ]
do
  sleep 5m &
  printf '%s' $! > "$TMP_DIR"/sleep.PID
  wait
  # PART X: code that executes every 5 mins
done

Then you can run:

kill $(< ~/.cache/myscript/sleep.PID)

Please note, that your PID file could be overwritten, e.g. by another instance of the same script and might not work reliably enough for your requirements.

16
  • 1
    pkill lets you select a pid by its parent process and other criteria -- no need to blindly kill all sleep 5m processes. As to printf ... > /tmp/sleep.PID being safe, it's simply sad that someone could believe that.
    – user414777
    Feb 8 '21 at 23:06
  • 1
    @user414777 Will you point out why printf ... > /tmp/sleep.PID this method is not safe? Feb 9 '21 at 6:02
  • 2
    Its good to comment that its not safe, but then please write at least the reason (i may improve the question), otherwise your comment is rather rude to me and does not help anyone.
    – pLumo
    Feb 9 '21 at 6:21
  • 2
    Thanks for pointing out. But I still don't see the real issue. When I create /tmp/sleep.PID, it's owned by me and has 644, so no one can overwrite it. And why should someone write something precious to /tmp. You might put it in ~/.cache/myscript/sleep.PID if you prefer.
    – pLumo
    Feb 9 '21 at 11:11
  • 2
    @user414777 It's ok to point out errors in people's code. It's not ok to say "it's sad people think this is fine" without actually saying why it isn't.
    – Sara J
    Feb 9 '21 at 12:00
8

I like @sitaram's method of using ionotifywait. However, a similar thing can be done using more ubiquitous infra - the shell read command (which can take an optional timeout in Bash/ksh/Zsh) and a FIFO.

The wait-loop looks something like this:

mkfifo /tmp/myfifo
while true; do
    read -t $((5*60)) <> /tmp/myfifo
    date
    echo "Executed every 5 minutes"
done

and the read is interrupted early simply by writing a newline to the FIFO:

echo > /tmp/myfifo

A couple of notes:

  1. A non-blocking redirect <> is used instead of a regular <. A regular redirect would block in Bash (until the FIFO is written to), before Bash ever starts the read, and so would never timeout if the FIFO was never written to.
  2. while [ true ] means test if the string "true" is non-empty and continue the while loop if it is non-empty. However while true means run the true command (which as you probably guessed) always returns true, and continue the while loop if it is true. In this case the result is exactly the same. However I think it is clearer without the [ ] test construct. Consider while [ false ] vs while false ...
  3. $((5*60)) is an arithmetic expansion. 300 could have just as easily been used, but this helps demonstrate how arithmetic may easily be performed in command lines.
4
  • 1
    Good answer. But don't forget to delete the fifo afterwards, they can hang around in some UNIXes (e.g. Solaris) and then give you an error on the (re)creation.
    – Kingsley
    Feb 9 '21 at 2:00
  • @Quasímodo Point taken regarding Posix. Still, FWIW, all zsh I can find does support read -t (MacOS 10.15.7 and Ubuntu 20.04.1), but you're certainly right about dash. /usr/bin/read does seem to work in all cases too though. Feb 9 '21 at 19:13
  • 1
    Indeed, you are right about Zsh, I missed it in the manual, sorry for that. POSIX only specifies -r for the standalone read. Thanks for the answers.
    – Quasímodo
    Feb 9 '21 at 19:17
  • @DigitalTrauma, I wouldn't count on /usr/bin/read being there in general, though. At least my Linuxen don't have it, and an external read is mostly useless anyway since it can't actually store the read value. (Here it would do since you ignore the read value.) My Mac has it, though, it just calls the read of /bin/sh there...
    – ilkkachu
    Feb 10 '21 at 14:10
4

If you have inotifywait available, you could setup a sentinel file (an empty file, with some unique name), and use inotifywait.

I.e., replace the sleep with this:

touch $HOME/.cache/sentinel
inotifywait -t 300 $HOME/.cache/sentinel

When you want to trigger this to execute "part X" right away, just touch $HOME/.cache/sentinel from some other terminal.

2

I think the easiest way would be to have the script check for the presence of a certain file. Then, in the while statement, sleep many times in shorter intervals. If you use seconds as your short interval, you would use a for loop that runs 300 times (=5 minutes). pseudo-code: for x=1 to 300 sleep 1 second check_if_certain_file_exists done From outside, you can create the file at any time and the script will notice it, within a one second interval. You can also create the file with different contents and have yout script perform different things based upon these.

2
  • I would probably go for a period of several seconds, though. I use this kind of method in a script to check my CPU temperature every 7 seconds (in an infinite loop) and increase/decrease my CPU frequency, aiming for a stable and reasonably low temperature. However, the kill command is its only interaction with the outside world... Feb 11 '21 at 1:28
  • If I rarely want to intercept the program, wouldn't this solution be highly inefficient? Feb 11 '21 at 14:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.