4

I want to implement something like this Q/A but for a sub-shell. Here is a minimal example of what I'm trying:

(subshell=$BASHPID
  (kill $subshell & wait $subshell 2>/dev/null) &
sleep 600)

echo subshell done

How can I make it so only subshell done returns instead of:

./test.sh: line 4:  5439 Terminated              ( subshell=$BASHPID; ( kill $subshell && wait $subshell 2> /dev/null ) & sleep 600 )
subshell done

Edit: I may be wrong on the terminology here, by subshell I mean the process within the first set of brackets.

Update:

I want to post the snippet from the actual program for context, above is a simplification:

# If subshell below if killed or returns error connected variable won't be set
(if [ -n "$2" ];then

      # code to setup wpa configurations here

      # If wifi key is wrong kill subshell
      subshell=$BASHPID
      (sudo stdbuf -o0 wpa_supplicant -Dwext -i$wifi -c/etc/wpa_supplicant/wpa_supplicant.conf 2>&1 \
        | grep -m 1 "pre-shared key may be incorrect" \
        && kill -s PIPE "$subshell") &

      # More code which does the setup necessary for wifi

) && connected=true

# later json will be returned based on if connected is set

3 Answers3

7

Note:

  • wait $subshell won't work as $subshell is not a child of the process you're running wait in. Anyway, you'd not waiting for the process doing the wait so it doesn't matter much.
  • kill $subshell is going to kill the subshell but not sleep if the subshell had managed to start it by the time kill was run. You could however run sleep in the same process with exec
  • you can use SIGPIPE instead of SIGTERM to avoid the message
  • leaving a variable unquoted in list contexts has a very special meaning in bash.

So having said all that, you can do:

(
  subshell=$BASHPID
  kill -s PIPE "$subshell" &
  sleep 600
)
echo subshell done

(replace sleep 60 with exec sleep 60 if you want the kill to kill sleep and not just the subshell, which in this case might not even have time to run sleep by the time you kill it).

In any case, I'm not sure what you want to achieve with that.

sleep 600 &

would be a more reliable way to start sleep in background if that's what you wanted to do (or (sleep 600 &) if you wanted to hide that sleep process from the main shell)

Now with your actual

sudo stdbuf -o0 wpa_supplicant -Dwext -i"$wifi" -c/etc/wpa_supplicant/wpa_supplicant.conf

command, note that sudo does spawn a child process to run the command (if only because it may need to log its status or perform some PAM session tasks afterwards). stdbuf will however execute wpa_supplicant in the same process, so in the end you'll have three processes (in addition to the rest of the script) in wpa_supplicant's ancestry:

  1. the subshell
  2. sudo as a child of 1
  3. wpa_supplicant (which was earlier running stdbuf) as a child of 2

If you kill 1, that doesn't automatically kills 2. If you kill 2 however, unless it's with a signal like SIGKILL that can't be intercepted, that will kill 3 as sudo happens to forward the signals it receives to the command it runs.

In any case, that's not the subshell you'd want to kill here, it's 3 or at least 2.

Now, if it's running as root and the rest of the script is not, you won't be able to kill it so easily.

You'd need the kill to be done as root, so you'd need:

sudo WIFI="$wifi" bash -c '
  (echo "$BASHPID" &&
   exec stdbuf -o0 wpa_supplicant -Dwext -i"$WIFI" -c/etc/wpa_supplicant/wpa_supplicant.conf 2>&1
  ) | {
    read pid &&
      grep -m1 "pre-shared key may be incorrect" &&
      kill -s PIPE "$pid"
  }'

That way, wpa_supplicant will be running in the same $BASHPID process as the subshell as we're making of that with exec.

We get the pid through the pipe and run kill as root.

Note that if you're ready to wait a little longer,

sudo stdbuf -o0 wpa_supplicant -Dwext -i"$wifi" -c/etc/wpa_supplicant/wpa_supplicant.conf 2>&1 |
  grep -m1 "pre-shared key may be incorrect"

Would have wpa_supplicant killed automatically with a SIGPIPE (by the system, so no permission issue) the next time it writes something to that pipe after grep is gone.

Some shell implementations would not wait for sudo after grep has returned (leaving it running in background until it gets SIGPIPEd), and with bash, you can also do that using the grep ... <(sudo ...) syntax, where bash doesn't wait for sudo either after grep has returned.

More at Grep slow to exit after finding match?

  • thanks processing this now, the sleep probably seems weird. In my actual code I have a process being grepped for errors, if found I run && kill. The sleep represents a longer code block I have that starts running but should be stopped in the case of particular errors. The echo is the success or failure json response (which I don't want to contain the process terminated text). – Philip Kirkbride Nov 26 '17 at 17:29
  • @PhilipKirkbride, if you wanted to kill a process as soon as it outputs something, see for instance Grep slow to exit after finding match? – Stéphane Chazelas Nov 26 '17 at 18:33
  • I think what you posted is the solution I need. I'm a bit confused about how you mentioned exec are you saying sleep will still be running in the background with what you posted? – Philip Kirkbride Nov 26 '17 at 20:38
  • I updated my question with an actual snippet, the sudo stdbuf -o0 wpa_supplicant ... would be the sleep. So I definitely want that process to end when I run kill -s PIPE "$subshell". – Philip Kirkbride Nov 26 '17 at 20:44
  • @PhilipKirkbride, see edit. – Stéphane Chazelas Nov 26 '17 at 21:05
  • Great answer, glad I posted the original as the sudo issue is a real gotcha in this situation. It will be funny adding /bin/bash to my sudoers file. I might have to look at running the whole script as sudo instead. Cheers! – Philip Kirkbride Nov 26 '17 at 21:23
  • @PhilipKirkbride, yes it would make more sense to add the script to sudoers here. See also zsh instead of bash to interpret your script, which has support for changing uids so could relinquish super user privileges for parts of the script that don't need them. It feels like there should be a cleaner solution by configuring wpa_supplicant to do what you want. If you ask a question about what you're trying to do in the end, you may get answers with better way to address the problem. – Stéphane Chazelas Nov 26 '17 at 21:29
  • Good point, it's probably out of scope here so I made a new post https://unix.stackexchange.com/questions/407168/wpa-supplicant-end-process-if-wrong-password-detected – Philip Kirkbride Nov 26 '17 at 21:50
2

Subshell refers to a shell command that is a child of some shell, such as child of the bash -i interactive login shell that offers you a $ prompt. You don't have to run your command in a subshell - you could choose to run it as an independent process. It sounds like this may be appropriate because you don't want its stdout / stderr messing up the appearance of your progress bar, and because you don't want the parent shell to report on or even notice the death of its child.

There are standard tools for accomplishing that, such as daemonize and nohup. (See also man pages.) You may be best off with nohup. Here is an example of using it to run a trivial program, which does not create nohup.out:

$ nohup true 2>&1 > /dev/null &

Have your program, or a wrapper script for your program, record its PID in /tmp/my.pid -- bash makes that available as the $$ variable. Then the monitoring process with the progress bar can

$ kill `cat /tmp/my.pid`

when it no longer needs that program to do any more processing. Alternatively, you might prefer to give your program name to killall.

J_H
  • 866
  • 1
    I don't think your idea of a subshell matches the meaning it's used in e.g. the Bash manual: "Command substitution, commands grouped with parentheses, and asynchronous commands are invoked in a subshell environment that is a duplicate of the shell environment, ... Builtin commands that are invoked as part of a pipeline are also executed in a subshell environment. Changes made to the subshell environment cannot affect the shell’s execution environment." – ilkkachu Nov 26 '17 at 17:27
  • When you type (cmd) or (foo|bar), your bash forks off a 2nd bash (a subshell process) that runs the command or pipeline. The benefit of the fork is you can change env variables for the subshell without disturbing their setting in the parent shell. There is a parent-child relationship between them that is usually helpful but seemed to be causing some small grief for the OP, which is why I suggested accomplishing the required cmd processing under nohup instead of under the parent shell. He was a bit vague on details of what that processing entails, so I went for a general, silent solution. – J_H Nov 26 '17 at 18:06
1

You may be looking for this

#!/bin/bash
(subshell=$BASHPID
  (kill $subshell & wait $subshell 2>/dev/null) &
sleep 600) &
wait $! 2>/dev/null

echo subshell done

here the subshell is put in background, then parent shell waits but with wait output sent to /dev/null. This catches the Terminated message.

Note, if you change wait to capture output to file e.g. wait $! 2>wait_output then you will see

 ./foo.sh: line 5:  1939 Terminated              ( subshell=$BASHPID; ( kill $subshell & wait $subshell 2> /dev/null ) & sleep 600 )

showing that Terminated is from parent shell.

A small check to see that it works if there is some activity before kill is

#!/bin/bash
(subshell=$BASHPID
 (sleep 1; kill $subshell & wait $subshell 2>/dev/null) &
sleep 600) & wait 2>wait_output

echo subshell done

This example will pause for a second before printing subshell done. This example also shows how to background and wait all on the same line e.g. & wait 2>wait_output. I am not sure if that has any advantage/disadvantage over the example with wait $!.

The key thing to note here is that the Terminated messages comes from the top-level parent shell job control. That is what sees the subshell terminate and generates the message. So that is where you want to catch the output. Redirecting the wait command output does this.