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:
- consumer (Wireshark) creates a named pipe (using
mkfifo
) and opens its reading end. - producer (a so-called Wireshark external capture program, aka extcap) gets started and the name/path of the named pipe passed.
- producer opens writing end as O_WRONLY and initially writes some data into the pipe.
- crickets
- user presses Stop button in Wireshark, Wireshark then closes its reading end of the pipe.
- ???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
}
}
}
select
ing /poll
ing onPOLLERR
, which is IIRC defined to happen when the reader closes its end of a FIFO? – Marcus Müller Dec 15 '23 at 15:20poll
shouldn't be a problem, right? – Marcus Müller Dec 15 '23 at 15:34POLLIN|POLLERR
, butpoll
only returns sometimes anEINTR
, but otherwise never anyPOLLERR
on the fd, despite the consumer side getting closed. I've pushed the new version to the link above. – TheDiveO Dec 15 '23 at 15:56