2

Say I have this:

node a.js | node b.js | node c.js

if a.js is about to die, can I kill b.js? What about vice versa? It seems like if I establish this pipe, one process won't necessarily die/exit if the other does. How can I kill 'em all if one dies?

  • What do you mean by "is about to die"? Who is "I" in this question? Is it the user, the script, or one of the parts of the pipeline? What's the use case here? Why would you want to explicitly kill bits of a pipeline? – Kusalananda Sep 20 '17 at 22:28
  • sorry to be vague, I mean programmatically, not the human user. – Alexander Mills Sep 20 '17 at 23:09
5

In:

cmd1 | cmd2

cmd2 doesn't automatically die when cmd1 finishes, however, it will see end-of-file on its stdin. And that's how pipelines usually come to their end.

In:

echo foo | sed s/o/e/g

sed will quit after echo has terminated because it finds that there's no more to read from its stdin then.

You could try and have cmd1 kill cmd2 upon terminating, but what if cmd2 has not read all what cmd1 has written to the pipe yet?

If cmd2 dies first, cmd1 does not automatically die, but it will be killed (with a SIGPIPE) the next time it tries to write to the (now broken) pipe.

That's how pipelines like:

yes | head

terminate (yes is killed upon the first write it does to the pipe after head has terminated after reading and printing the first 10 lines).

Now, if you really wanted to kill processes at the other end of the pipe, there is no portable way to find out what process(es) have a file descriptor to the other end of a pipe.

On Linux, you can look for the files in /proc/*/fd that have the same inode as the pipe on the fd to your end of the pipe and where the permissions of the symlink itself indicates which end of the pipe it is.

$ (ls -lLi /proc/self/fd/3; ls -l /proc/self/fd/3) 3>&1 >&2 | (ls -Lil /proc/self/fd/0; ls -l /proc/self/fd/0)
224052 prw------- 1 chazelas chazelas 0 Sep 20 23:26 /proc/self/fd/3|
224052 prw------- 1 chazelas chazelas 0 Sep 20 23:26 /proc/self/fd/0|
l-wx------ 1 chazelas chazelas 64 Sep 20 23:26 /proc/self/fd/3 -> pipe:[224052]
lr-x------ 1 chazelas chazelas 64 Sep 20 23:26 /proc/self/fd/0 -> pipe:[224052]

Same pipe inode, and the fd to the writing end has w permissions, while the one on the reading end has r permissions.

For instance, with zsh you could get the pids of the processes that have a fd at the reading end of a pipe whose other end is on our fd 3 with:

pids=(/proc/<->/fd/*(Nf{u+r}e{'[[ $REPLY -ef /dev/fd/3 ]]'}-p:h:h:t))
pids=(${(u)pids}) # unique

Example:

$ (pids=(/proc/<->/fd/*(Nf{u+r}e{'[[ $REPLY -ef /dev/fd/3 ]]'}-p:h:h:t))
  pids=(${(u)pids}); ps -fp $pids) 3>&1 >&2 | tr a b
UID        PID  PPID  C STIME TTY          TIME CMD
chazelas 24157 11759  0 23:41 pts/1    00:00:00 tr a b

So you could do (still on Linux and with zsh):

run_and_kill_the_other_end() {
  setopt localoptions localtraps
  trap : TERM
  "$@"
  local ret=$?
  local -aU pids
  if [ -p /dev/stdout ]; then
    pids=(/proc/<2->/fd/<0-9>(Nf{u+r}e{'[[ $REPLY -ef /dev/stdout ]]'}-p:h:h:t))
    exec >&- # give the right end a chance to see eof and act upon it
  fi
  [ -p /dev/stdin ] &&
    pids+=(/proc/<2->/fd/<0-9>(Nf{u+w}e{'[[ $REPLY -ef /dev/stdin ]]'}-p:h:h:t))
  (($#pids)) && kill $pids 2> /dev/null
  return $ret
}

And then:

run_and_kill_the_other_end node a.js | run_and_kill_the_other_end node b.js

Though again, that's probably not what you want to do. For instance:

$ run_and_kill_the_other_end seq 100 |
    run_and_kill_the_other_end sort |
    run_and_kill_the_other_end wc -l
0

sort was killed before it had the chance to write its sorted output. wc -l managed to escape.

You could insert a delay as a grace period, but how long should the delay be? For instance, if I add a sleep 0.1 after the exec >&-, I see:

$ run_and_kill_the_other_end seq 100 | run_and_kill_the_other_end sort | run_and_kill_the_other_end wc -l
100
$ run_and_kill_the_other_end seq 10000 | run_and_kill_the_other_end sort | run_and_kill_the_other_end wc -l
10000
$ run_and_kill_the_other_end seq 100000 | run_and_kill_the_other_end sort | run_and_kill_the_other_end wc -l
0

That also means delaying the termination of the pipeline unconditionally. You may be able to improve that by using zsh's zselect with a timeout instead.

  • this is a nice answer to a very nice question – Alexander Mills Sep 20 '17 at 23:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.