9

Is there any way for a writer to know that a reader has closed its end of a named pipe (or exited), without writing to it?

I need to know this because the initial data I write to the pipe is different; the reader is expecting an initial header before the rest of the data comes.

Currently, I detect this when my write() fails with EPIPE. I then set a flag that says "next time, send the header". However, it is possible for the reader to close and re-open the pipe before I've written anything. In this case, I never realize what he's done, and don't send the header he is expecting.

Is there any sort of async event type thing that might help here? I'm not seeing any signals being sent.

Note that I haven't included any language tags, because this question should be considered language-agnostic. My code is Python, but the answers should apply to C, or any other language with system call-level bindings.

Jonathon Reinhart
  • 132,704
  • 33
  • 254
  • 328
  • man 2 write : `EPIPE` `fd is connected to a pipe or socket whose reading end is closed. When this happens the writing process will also receive a SIG‐ PIPE signal. (Thus, the write return value is seen only if the program catches, blocks or ignores this signal.)` – wildplasser Feb 11 '15 at 23:37
  • Any reason not to send the header data periodically? If the reader can deal with them, maybe it can deal with redundant headers? – wallyk Feb 12 '15 at 00:59
  • @wildplasser I'm not sure what point you're making right now. Yes, I know the pipe is closed when I try to write to it. My question is asking how to asynchronously detect this without writing. – Jonathon Reinhart Feb 12 '15 at 02:49
  • @wallyk That's not an option here. In this case, the other end of the pipe is Wireshark, who's expecting pcap data. The initial header tells Wireshark the type of data, while the rest of the data are packets. – Jonathon Reinhart Feb 12 '15 at 02:50

3 Answers3

4

If you are using an event loop that is based on the poll system call you can register the pipe with an event mask that contains EPOLLERR. In Python, with select.poll,

import select
fd = open("pipe", "w")
poller = select.poll()
poller.register(fd, select.POLLERR)
poller.poll()

will wait until the pipe is closed.

To test this, run mkfifo pipe, start the script, and in another terminal run, for example, cat pipe. As soon as you quit the cat process, the script will terminate.

Phillip
  • 13,448
  • 29
  • 41
  • What is `poller` here? – Jonathon Reinhart Feb 16 '15 at 12:46
  • Sory, I forgot to add that line. It's a `select.poll` object. – Phillip Feb 16 '15 at 14:05
  • The second argument should be `select.POLLERR`, not `select.EPOLLERR`. Also, have you actually tried this? My testing with `select` ([below](http://stackoverflow.com/a/28472725/119527)) indicates that the pipe surprisingly becomes *readable*, not *exceptional* when the other end is closed. – Jonathon Reinhart Feb 19 '15 at 05:11
  • I've gone ahead and awarded the bounty to you, because your answer put me on the right track. I'll wait until I have a working example using a `select.poll` object however, to accept your answer. – Jonathon Reinhart Feb 19 '15 at 05:14
  • Indeed, I've mixed up the [epoll and poll constants](https://docs.python.org/2/library/select.html?highlight=select.poll#epoll-objects), but luckily both constants have the value `8`. I've fixed that, and also another typo. Anyway, `POLLERR` is correct - the `poll()` API sets an error state while the `select()` API reports availability to read, I don't know why there's a difference, maybe for historical reasons. – Phillip Feb 19 '15 at 08:21
3

Oddly enough, it appears that when the last reader closes the pipe, select indicates that the pipe is readable:

writer.py

#!/usr/bin/env python
import os
import select
import time

NAME = 'fifo2'

os.mkfifo(NAME)


def select_test(fd, r=True, w=True, x=True):
    rset = [fd] if r else []
    wset = [fd] if w else []
    xset = [fd] if x else []

    t0 = time.time()
    r,w,x = select.select(rset, wset, xset)

    print 'After {0} sec:'.format(time.time() - t0)
    if fd in r: print ' {0} is readable'.format(fd)
    if fd in w: print ' {0} is writable'.format(fd)
    if fd in x: print ' {0} is exceptional'.format(fd)

try:
    fd = os.open(NAME, os.O_WRONLY)
    print '{0} opened for writing'.format(NAME)

    print 'select 1'
    select_test(fd)

    os.write(fd, 'test')
    print 'wrote data'

    print 'select 2'
    select_test(fd)

    print 'select 3 (no write)'
    select_test(fd, w=False)

finally:
    os.unlink(NAME)

Demo:

Terminal 1:

$ ./pipe_example_simple.py
fifo2 opened for writing
select 1
After 1.59740447998e-05 sec:
 3 is writable
wrote data
select 2
After 2.86102294922e-06 sec:
 3 is writable
select 3 (no write)
After 2.15910816193 sec:
 3 is readable

Terminal 2:

$ cat fifo2
test
# (wait a sec, then Ctrl+C)
Jonathon Reinhart
  • 132,704
  • 33
  • 254
  • 328
1

There is no such mechanism. Generally, according to the UNIX-way, there are no signals for streams opening or closing, on either end. This can only be detected by reading or writing to them (accordingly).

I would say this is wrong design. Currently you are trying to have the receiver signal their availability to receive by opening a pipe. So either you implement this signaling in an appropriate way, or incorporate the "closing logic" in the sending part of the pipe.

Prikso NAI
  • 2,592
  • 4
  • 16
  • 29
  • 1
    `"There is no such mechanism."` Did you see my answer above, which clearly indicates that you are wrong? – Jonathon Reinhart Feb 19 '15 at 05:10
  • The `poll` and `select` calls are used to detect when an FD is ready for reading/writing, in order to avoid blocking. There is still no signaling information on streams opening or closing, and in order to detect this you would need to perform a read/write or a _test_ for read/write readiness. I admit this information is left out. The answer does not change, nonetheless, as polling is still not a signaling mechanism, and the race conditions you describe (the pipe closing and re-opening before polling/reading/writing) is still there with `poll`. It could be reopened before you poll it. – Prikso NAI Feb 19 '15 at 08:50