A Weird Imagination

Killing pipes from within

The problem#

Last week, I mentioned that I needed a hack to kill xprop that seemed like it should be unnecessary. Specifically, I had its output piped to a Bash while read loop and once that had found a line to act on, there was no further need to get more lines from xprop, but break or exit didn't result in xprop exiting.

The solution#

Use $BASHPID to get the actual PID of the subshell, ps to climb the process tree to the appropriate parent and pkill to kill its children:

some_pipeline |
  while read -r line
  do
    # Do whatever until ready to kill the pipe...
    # ... then kill it:
    ppid=$BASHPID
    ppid=$(ps -o ppid:1= "$ppid")
    pkill -9 -P "$ppid" 
  done

Rewrite appfinder-and-focus.sh from last time:

#!/usr/bin/bash
xprop -spy -root _NET_CLIENT_LIST | stdbuf -oL head -2 |
  while read -r l
  do
    winid="${l/#*, /}"
    if [[ $(xdotool getwindowname "$winid") == \
      "Application Finder" ]]
    then
      xdotool windowactivate "$winid"
      ppid=$BASHPID
      ppid=$(ps -o ppid:1= "$ppid")
      pkill -9 -P "$ppid" 
    fi
  done) 2>/dev/null &
xfce4-appfinder &

The details#

Checking if xprop is running#

While testing this, I wanted to be able to keep an eye on whether each variant left xprop running or not, so I left a terminal open using watch to continually show the current xprop processes using brackets so the grep won't get included:

watch 'ps aux | grep [x]prop'

Although it's simpler to just use pgrep:

watch 'pgrep -a xprop'

If it is?#

I did a lot of

killall xprop

when that showed it was in fact still running.

Examining the process tree#

In order to find the right process to kill, I needed to understand what the process tree looked like. To do that, I used htop in "tree" (as opposed to "list") mode.1 pstree can also be used to visualize process trees, although without the interactive search, it's less obvious how to get it to how the full context. One idea might be to have it output the full process tree and use grep to display a few lines around the reference to "xprop":

watch 'pstree -p | grep -A2 -B2 -F xprop'

which outputs something like

  |                `-{apcupsd}(36607)
  |-appfinder-and-f(244817)-+-appfinder-and-f(244820)
  |                         `-xprop(244819)
  |-at-spi-bus-laun(3816)-+-dbus-daemon(3822)
  |                       |-{at-spi-bus-laun}(3817)

(Adjust those 2s as needed if 2 lines of context above/below is not enough.)

Killing the pipe#

Exactly what a pipe means a little unintuitive—as uses of parallelism tend to be. Every program in the pipe runs at once, so there's no meaningful concept of stopping a pipe partway through. There's Bash's "pipefail" option, but it only changes the return value. This StackExchange answer suggested running kill from inside the middle of a piped command, and it's what my solution is based on.

Getting the subshell PID#

The current shell's PID is $$. But some constructs fork off a subshell with a different PID but the same value of $$. In Bash, $BASHPID gives the PID of the current subshell, and that link includes a discussion of how to do the same in other shells which I did not bother with because my script was already Bash-only.

But wait, if I add a debugging line

pstree -p | grep -A2 -B2 -F xprop

inside the while read loop, then it shows a tree like this:

  |-appfinder-and-f(244817)-+-appfinder-and-f(244820)-+-grep(244853)
  |                         |                         `-pstree(244852)
  |                         `-xprop(244819)

and echo $BASHPID indeed prints the PID 244820, not 244817 which is the parent of xprop which is the PID we actually want.

Getting the parent PID#

ps can be used to get the parent PID of a PID:

ppid=$(ps -o ppid= "$BASHPID")

There's two problems with that: first, $() creates a subshell, so $BASHPID is redefined in that context, so that line is actually equivalent to

ppid=$BASHPID

So we need to save $BASHPID to a separate variable before running it:

ppid=$BASHPID
ppid=$(ps -o ppid= "$ppid")

The second is more subtle. Here's the current code with some debugging echos to show the values:

ppid=$BASHPID
ppid=$(ps -o ppid= "$ppid")
echo "BASHPID=$BASHPID"
echo "pid=$ppid"

And here's what it outputs:

BASHPID=244820
pid= 244817

See the problem? That space is not a typo. The issue is that ps is not really designed for this, and part of making its output look nice for humans is that it adds spaces to line up the columns in its output. As its output here is only a single number, that doesn't really make sense, but it's doing it anyway. Looking through the man page, I found there's an option to explicitly set the column widths, and just force setting it to 1 avoids the issue:

ppid=$(ps -o ppid:1= "$ppid")

Then we get the number in the format we wanted:

pid=244817

Killing the children#

Now that we have the PID of the subshell that started the pipe, we can use pkill's -P option to kill all of the processes that make up the pipe:

pkill -9 -P "$ppid" 

head instead of tail#

If you compare the solution in this post to the one last time, you'll notice I removed | stdbuf -oL tail -n +2 and replaced it with a call to head instead. The reason for this is that if the "Application Finder" window is already open when the script runs, then it will never observe the window opening and therefore never trigger the logic to exit. This wasn't a problem in the previous version of the script because the logic to exit just checked that the "Application Finder" window was already open.

This way, if the "Application Finder" window is the most recent window to be opened, it will still get focused. And also, after two window open/close actions, the pipe will terminate anyway.

Of course, the fact that this works, means I could have just accepted the xprop process would hang around at worst until the next time I opened or closed a window without going through the complexity of making sure to kill it as soon as it was no longer needed. But then what would I have to write a blog post about? there might be other scenarios where being able to kill a pipeline early would be useful.


  1. F5 swaps htop's view between list and tree mode. The label on F5 on the bottom row shows the mode it will switch into not the mode it is currently in. 

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.