3

I have a backgrounded process that I would like to wait for (in case it fails or dies), unless I receive user input. Said another way, the user input should interrupt my waiting.

Here's a simplified snippet of my code

#!/bin/bash
...

mplayer -noconsolecontrols "$media_url" &
sleep 10 # enough time for it to fail

ps -p $!
if [ $? -ne 0 ]
then
    fallback
else
    read
    kill $!
fi

The line that I particularly dislike is sleep 10, which is bad because it could be too much time, or not enough time.

Is there a way to wait $! || read or the equivalent?

Michael Plotke
  • 951
  • 1
  • 15
  • 38

3 Answers3

3

Use kill -0 to validate that the process is still there and read with a timeout of 0 to test for user input. Something like this?

pid=$!
while kill -0 $pid; do
    read -t 0 && exit
    sleep 1
done
Mike Andrews
  • 3,045
  • 18
  • 28
  • 2
    This introduces a pretty bad race condition. Suppose the process dies while sleeping, then another process is started with the same pid. You'd be watching a different (random) process! – mvds Jul 02 '15 at 02:20
2

Original

ps -p to check the process. read -t 1 to wait for user input.

pid=$!
got_input=142
while ps -p $pid > /dev/null; do
    if read -t 1; then
        got_input=$?
        kill $pid
    fi
done

This allows for branching based whether the process died, or was killed due to user input.


All credit to gubblebozer. The only reason I'm posting this answer is the claim by moderators that my edits to his post constituted altering his intent.

Anti Race-Condition

First off, a race condition involving pids is (very likely) not a concern if you're fairly quick, because they're reused on a cycle.

Even so, I guess anything is possible... Here's some code that handles that possibility, without breaking your head on traps.

got_input=142

while true; do
    if read -t 1; then
        got_input=$?
        pkill --ns $$ name > /dev/null
        break
    elif ! pgrep --ns $$ name > /dev/null; then
        break
    fi
done

Now, we've accomplished our goal, while (probably) completely eliminating the race condition.

Community
  • 1
  • 1
Michael Plotke
  • 951
  • 1
  • 15
  • 38
  • 2
    This introduces a pretty bad race condition. Suppose the process dies while waiting, then another process is started with the same pid, and then the user hits enter. You'd be killing a random process. – mvds Jul 02 '15 at 02:10
  • @mvds Interesting. Let me ask. If I say directly `if ps -p $pid; then kill $pid; fi`, is that guaranteed to kill the correct process? – Michael Plotke Jul 02 '15 at 02:13
  • Still not entirely, you should adopt a different approach. It's better to not "poll" yourself, but to wait for a signal. For more reasons than this race condition alone! See my answer. – mvds Jul 02 '15 at 02:18
  • Pids are recycled in a pretty short cycle of 32k, so a collision *will* happen on any system doing intensive work (things like `find ... -exec` will make the pid counter wrap around fast). This is just bad design. If you intend to wait for an event, wait for the event. – mvds Jul 02 '15 at 15:44
  • 1
    This is pretty great, man. You're right, the race that @mvds brought up is still there, between `pgrep` and `kill`. But it's incredibly tiny. Could use `pkill`? But that's a minor point, my bet is that what you ended up with will suit your purposes! – Mike Andrews Jul 02 '15 at 17:27
  • The fact the pid's are reused in a cycle *is the very reason it's a race condition*. Sorry to say but there really is no way out of it, you have to keep a tap on the child process, even before it is started, or something might happen while your script is sleeping (either in a `sleep` or in a `read`). I see no other way than signal handling for it. Maybe you could abuse /proc/pid in some unusual way, like opening /proc/pid/cmdline and then detecting the fd is closed because /proc/pid disappeared. – mvds Jul 02 '15 at 22:03
0

Any loop with a sleep or similar timeout in it, will introduce a race condition. It's better to actively wait for the process to die, or, in this case, to trap the signal that's sent when a child dies.

#!/bin/bash

set -o monitor
trap stop_process SIGCHLD
stop_process()
{
    echo sigchld received
    exit
}

# the background process: (this simulates a process that exits after 10 seconds)
sleep 10 &

procpid=$!
echo pid of process: $procpid

echo -n hit enter: 
read

# not reached when SIGCHLD is received
echo killing pid $procpid
kill $procpid

I'm not 100% sure this eliminates any race condition, but it's a lot closer than a sleep loop.

edit: the shorter, less verbose version

#!/bin/bash

set -o monitor
trap exit SIGCHLD

sleep 5 &

read -p "hit enter: "
kill $!

edit 2: setting the trap before starting the background process prevents another race condition in which the process would die before the trap was installed

mvds
  • 45,755
  • 8
  • 102
  • 111
  • Maybe the docs are misleading, but `monitor -m` seems superfluous, and `trap exit SIGCHILD` seems wrong. Also, running it doesn't result in the expected behavior. So... – Michael Plotke Jul 02 '15 at 02:49
  • I don't follow you. Indeed `trap exit SIGCHILD` is wrong because of the superfluous `I` you added. The manpage states that `trap` is followed by a `command`. The manpage also states that `exit` is a `command`. So could you please elaborate on how this "seems wrong"? About `monitor` you may be right. – mvds Jul 02 '15 at 15:15
  • You're correct about `SIGCHLD` (was having trouble finding it), but even so this does not work. I spent hours on simple test cases getting nowhere. – Michael Plotke Jul 02 '15 at 15:19
  • Maybe I don't understand the expected behavior then. I read it as: 1) User input should kill the background process, 2) if the background process dies, don't wait for user input anymore – mvds Jul 02 '15 at 15:45
  • Just tested: it indeed doesn't work if you don't call `set -o monitor`, so it doesn't really seem superfluous. Apparently you do have to explicitly "enable job control" to get `SIGCHLD` signals. – mvds Jul 02 '15 at 15:55
  • Maybe our systems are different, because here it does **not** work, even with `set -o monitor`. `exit` doesn't get called until after `read` receives input. – Michael Plotke Jul 02 '15 at 16:11
  • Strange. Tested on a recent and an outdated Debian stable and Mac OS X 10.10 (*GNU bash, version 4.2.37(1)-release (x86_64-pc-linux-gnu)*, *GNU bash, version 4.1.5(1)-release (i486-pc-linux-gnu)* and *GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin14)*) – mvds Jul 02 '15 at 21:46
  • @MichaelPlotke but do you even *want* this to work..? – mvds Jul 02 '15 at 22:05
  • I would be thrilled if this worked. Gave up when I found this confirmation of my experience. "When Bash receives a signal for which a trap has been set while waiting for a command to complete, the trap will not be executed until the command completes." - [tldp](http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html#sect_12_02_02) – Michael Plotke Jul 02 '15 at 22:26
  • Even then it would only mean you'd need to loop the `read`, so that the trap can be handled between `read` calls. – mvds Jul 02 '15 at 22:57