3

Note: this is about the producer detecting the consumer disconnecting, not the consumer detecting that the producer disconnected (aka EOF). The producer might not write any data for long periods, but want to quickly find out that the consumer has disconnected.

General Sequence of Events:

  1. consumer (Wireshark) creates a named pipe (using mkfifo) and opens its reading end.
  2. producer (a so-called Wireshark external capture program, aka extcap) gets started and the name/path of the named pipe passed.
  3. producer opens writing end as O_WRONLY and initially writes some data into the pipe.
  4. crickets
  5. user presses Stop button in Wireshark, Wireshark then closes its reading end of the pipe.
  6. ???trying to detect in producer that consumer has disconnected from named pipe, even if producer has no data to send???

Linux

On Linux in the producer, using the select syscall with the fd for the write end of the named pipe in the read fd set will return the writing end fd become readable upon the consumer disconnecting.

MacOS

However, on macos, the write end fd of the named pipe becomes readable(sic!) whenever the producer writes data. It does not become readable upon the consumer disconnecting.

EDIT: Adding an error fd set doesn't change the situation; there is never an error fd set. /EDIT

Any ideas as to how detect the consumer disconnecting from a named pipe on macos, without SIGPIPE as there might be no writes for a long time, but the user already stopped Wireshark recording?

Consumer Disconnect Detection on Named Pipe for Producer

https://github.com/siemens/cshargextcap/blob/macos/pipe/checker_notwin.go

package pipe import ( "os" "golang.org/x/sys/unix" log "github.com/sirupsen/logrus" ) // WaitTillBreak continuously checks a fifo/pipe to see when it breaks. When // called, WaitTillBreak blocks until the fifo/pipe finally has broken. // // This implementation leverages [syscall.Select]. func WaitTillBreak(fifo *os.File) { log.Debug("constantly monitoring packet capture fifo status...") fds := unix.FdSet{} for { // Check the fifo becomming readable, which signals that it has been // closed. In this case, ex-termi-nate ;) Oh, and remember to correctly // initialize the fdset each time before calling select() ... well, just // because that's a good idea to do. :( fds.Set(int(fifo.Fd())) n, err := unix.Select( int(fifo.Fd())+1, // highest fd is our file descriptor. &fds, nil, nil, // only watch readable. nil, // no timeout, ever. ) if n != 0 || err != nil { // Either the pipe was broken by Wireshark, or we did break it on // purpose in the piping process. Anyway, we're done. log.Debug("capture fifo broken, stopped monitoring.") return } } } 

Unit Test That Produces Incorrect Behavior on MacOS

https://github.com/siemens/cshargextcap/blob/macos/pipe/checker_notwin_test.go -- fails the assertion that WaitTillBreak must not return before we actually closed the consumer end of the named pipe.

package pipe import ( "io" "os" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/thediveo/success" "golang.org/x/sys/unix" ) var _ = Describe("pipes", func() { It("detects on the write end when a pipe breaks", func() { // As Wireshark uses a named pipe it passes an extcap its name (path) // and then expects the extcap to open this named pipe for writing // packet capture data into it. For this test we simulate Wireshark // closing its reading end and we must properly detect this situation on // our writing end of the pipe. By("creating a temporary named pipe/fifo and opening its ends") tmpfifodir := Successful(os.MkdirTemp("", "test-fifo-*")) defer os.RemoveAll(tmpfifodir) fifoname := tmpfifodir + "/fifo" unix.Mkfifo(fifoname, 0660) wch := make(chan *os.File) go func() { defer GinkgoRecover() wch <- Successful(os.OpenFile(fifoname, os.O_WRONLY, os.ModeNamedPipe)) }() rch := make(chan *os.File) go func() { defer GinkgoRecover() rch <- Successful(os.OpenFile(fifoname, os.O_RDONLY, os.ModeNamedPipe)) }() var r, w *os.File Eventually(rch).Should(Receive(&r)) Eventually(wch).Should(Receive(&w)) defer w.Close() go func() { defer GinkgoRecover() By("continously draining the read end of the pipe into /dev/null") null := Successful(os.OpenFile("/dev/null", os.O_WRONLY, 0)) defer null.Close() io.Copy(null, r) By("pipe draining done") }() go func() { defer GinkgoRecover() time.Sleep(2 * time.Second) By("closing read end of pipe") Expect(r.Close()).To(Succeed()) }() go func() { defer GinkgoRecover() time.Sleep(300 * time.Microsecond) By("writing some data into the pipe") w.WriteString("Wireshark rulez") }() By("waiting for pipe to break") start := time.Now() WaitTillBreak(w) Expect(time.Since(start).Milliseconds()).To( BeNumerically(">", 1900), "pipe wasn't broken yet") }) }) 

Poll-Based Version

This doesn't work on macos either, never returning any POLLERR. It does work correctly on Linux, however.

package pipe import ( "os" "golang.org/x/sys/unix" log "github.com/sirupsen/logrus" ) // WaitTillBreak continuously checks a fifo/pipe to see when it breaks. When // called, WaitTillBreak blocks until the fifo/pipe finally has broken. // // This implementation leverages [unix.Poll]. func WaitTillBreak(fifo *os.File) { log.Debug("constantly monitoring packet capture fifo status...") fds := []unix.PollFd{ { Fd: int32(fifo.Fd()), Events: 0, }, } for { // Check the fifo becomming readable, which signals that it has been // closed. In this case, ex-termi-nate ;) Oh, and remember to correctly // initialize the fdset each time before calling select() ... well, just // because that's a good idea to do. :( n, err := unix.Poll(fds, 1000 /*ms*/) if err != nil { if err == unix.EINTR { continue } log.Debugf("capture fifo broken, reason: %s", err.Error()) return } if n <= 0 { continue } log.Debugf("poll: %+v", fds) if fds[0].Revents&unix.POLLERR != 0 { // Either the pipe was broken by Wireshark, or we did break it on // purpose in the piping process. Anyway, we're done. log.Debug("capture fifo broken, stopped monitoring.") return } } } 
6
  • shouldn't you be selecting / polling on POLLERR, which is IIRC defined to happen when the reader closes its end of a FIFO? Commented Dec 15, 2023 at 15:20
  • Select doesn't report any error and no fd from the error set either. Is POLLERR only available for poll, correct? Commented Dec 15, 2023 at 15:28
  • Sounds like a dup of How to find out if pipe is broken? except for the macos constraint. Commented Dec 15, 2023 at 15:31
  • @TheDiveO hm, but having to use poll shouldn't be a problem, right? Commented Dec 15, 2023 at 15:34
  • 1
    @Marcus Müller: specified fd events as POLLIN|POLLERR, but poll only returns sometimes an EINTR , but otherwise never any POLLERR on the fd, despite the consumer side getting closed. I've pushed the new version to the link above. Commented Dec 15, 2023 at 15:56

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.