26

This may sound trivial, but I'm pretty sure this question hasn't been asked, or at least I can't find it.

I'm looking for a way to construct an infinite wait (not necessarily a loop) with shell scripting so that it waits forever and can be killed (or technically, to receive a SIGTERM). The following are known possible constructs and arguments against them:

  1. while true; do sleep 1; done This almost gets it, but since sleep is an external command, when I send a SIGTERM to the running script, it has to wait for the sleep to finish first and then process the signal. Change sleep 1 to something like sleep 10 and the lag will be obvious. Also the solution wakes up the CPU every 1 second, which is not ideal.
  2. while true; do read; done This is perfect when stdin is tty. read is a shell builtin and SIGTERM arrives at the script instantly. But, when stdin is /dev/null, the script eats up all the CPU by helplessly running read forever on /dev/null.

Thus a shell builtin construct that waits forever is required. Skimming through man dash I didn't find such one - the only blocking builtins are read and wait, and I don't have idea how I can construct an ideal one using wait.

The answer should be applicable to POSIX shell (effectively dash), or less preferably, Bash.

Additional notes.

The situation where the first example doesn't work perfectly is more complex than I thought. With the following shell script:

#!/bin/sh
echo $$
while true; do
    sleep 100
done

if you kill it at another tty, it terminates immediately. The funny thing begins when you attempt to do trapping. With this script:

#!/bin/sh
at_term() {
    echo 'Terminated.'
    exit 0
}
trap at_term TERM
echo $$
while true; do
    sleep 20
done

What happens is exactly described in example 1. This happens with bash, dash and zsh. And it's under this condition that I'm seeking a "perfect" infinite look construct.

xiaq
  • 764
  • 1
  • 6
  • 13
  • 2
    When I send a `SIGTERM` to a `dash` executing `sleep 100` in a loop, it exits immediately. – Fred Foo Jan 29 '12 at 11:51
  • What do you require this for? If the process should persist but do nothing, it could send itself a `SIGSTOP`. – tripleee Jan 29 '12 at 18:09

7 Answers7

18

you can use a named pipe for your read:

mkfifo /tmp/mypipe
#or mknode /tmp/mypipe p

if you later want to send different arbitrary "signals" to the pipe, the read can be use in combination with a case statement to take appropriate actions (even useful ones)

while read SIGNAL; do
    case "$SIGNAL" in
        *EXIT*)break;;
        *)echo "signal  $SIGNAL  is unsupported" >/dev/stderr;;
    esac
done < /tmp/mypipe
technosaurus
  • 7,676
  • 1
  • 30
  • 52
  • Yes, this one is virtually perfect. I'll see if someone can came up with something simpler or I'll accept... – xiaq Feb 01 '12 at 15:32
  • 1
    Accepted your answer :) It's universal under all circumstances. – xiaq Feb 04 '12 at 16:53
14

Here's a solution without a loop:

#!/usr/local/bin/dash

echo $$

# -$$: kill process group (parent and children)
#trap 'trap - TERM; kill 0' TERM
#trap 'trap - INT TERM; kill 0' INT TERM

trap 'trap - TERM; kill -s TERM -- -$$' TERM

tail -f /dev/null & wait

exit 0
carlo
  • 141
  • 1
  • 2
  • 1
    Your script is magic! it is the only one which does what I want (response to signal 15). Could you explain it in details please? so why you repeated trap in trap? the whole things written in trap are not clear for me, and why & after tail, and why wait after all? – Mohammed Noureldin Jan 18 '17 at 01:07
  • @MohammedNoureldin The `trap - TERM` part removes the existing trap handler for the script, so that the same signal won't be caught again. The `kill -s TERM -- -$$` part sends `SIGTERM` to all processes in the same process group. Effectively all child and descendant processes (including the script itself). I.e. a negative PID is a PGID as far as `kill` is concerned, but we need an extra `--` to not confuse with command switches. As the `SIGTERM` handler has been removed, the script will terminate when receiving the second `SIGTERM` signal (that it sends in the old handler). – Per Cederberg Dec 16 '17 at 21:14
10

If you have GNU coreutils, which accepts floating-point seconds, you can try:

sleep inf

This should block until the 64-bit timestamp wraparound.

yingted
  • 9,996
  • 4
  • 23
  • 15
  • if you don't have `inf` (e.g. on alpine/sh), you can always just put this in an infinite loop. Either way, this works a treat – Jon Bates Mar 04 '19 at 15:43
  • This doesn't work if only the shell script (and not the *sleep(1)* process) receives the signal. – pts Jan 13 '21 at 12:58
2

What's wrong with your 2nd option but forcing it to read from stdin ? (Requires bash)

while true; do
  read
done < /dev/stdin

From man bash

Bash handles several filenames specially when they are used in redirections, as described in the following table:

          /dev/stdin
                 File descriptor 0 is duplicated.
SiegeX
  • 135,741
  • 24
  • 144
  • 154
  • I cannot find much documentation about /dev/stdin, therefore it's reasonable to suspect it may not be always available. – xiaq Feb 01 '12 at 14:58
  • It would be very nice if someone can came up with some documentation regarding /dev/stdin :) – xiaq Feb 01 '12 at 15:34
  • 3
    It breaks when stdin is redirected to /dev/null. Put that in a.sh and run `bash a.sh < /dev/null` and see the CPU being eaten... – xiaq Feb 04 '12 at 16:51
1
#!/bin/sh
at_term() {
  echo 'Terminated.'
  exit 0
}
trap at_term TERM
echo $$
while true; do
  sleep 20 &
  wait $!
done
clacke
  • 7,688
  • 6
  • 46
  • 48
  • Thanks for the solution! Personally I think it's more elegant than using `read`, and it works perfectly! – Haravikk May 14 '13 at 11:54
0

Why do not you use sleep forever? This is designed to forever block.

Daniel Hornik
  • 1,957
  • 1
  • 14
  • 33
0

SIGTERM sent to a process is delivered by the kernel to the process whether it is sleeping or not.

Try experimenting, maybe like this (bash example)

sleep 20 &
kill $! && fg
jim mcnamara
  • 16,005
  • 2
  • 34
  • 51