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 2
s 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
echo
s 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.
-
F5 swaps
htop
's view between list and tree mode. The label onF5
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.