Reputation: 4000
I'm writing a Jupyter kernel for Go, and before executing the Go code I create a side named pipe (syscall.Mkfifo
) as a mechanism to allow one to publish html, images, etc.
My kernel creates the fifo, and then opens it for reading in a new goroutine and polls for input accordingly. Now, opening a fifo for reading is a blocking operation (it waits until someone opens on the other side).
But some programs are not interested in using this mechanism and will never open the other side of the fifo. When this happens my goroutine leaks, and it forever waits on the opening fifo -- even after I remove it.
The code looks more or less (less error handling) like this:
...
syscall.Mkfifo(pipePath, 0600)
go func() {
pipeReader, _ := os.Open(pipePath)
go poll(pipeReader) // Reads and process each entry until pipeReader is closed.
<-doneChan
pipeReader.Close()
}
go func() {
<-doneChan
os.Remove(pipePath)
}
Where doneChan
is closed when the program I start finishes executing.
And the issue is that os.Open(pipePath)
never returns if the other end is never opened,
even though the os.Remove(pipePath)
is properly executed.
Is there a way to forcefully interrupt os.Open(pipePath)
, or another way to achieving the same thing ?
Thanks in advance!!
Upvotes: 3
Views: 544
Reputation: 1823
I think the simplest way to handle FIFOs in go is to exec a sub-process:
exec.CommandContext(ctx, "cat", fifoPath)
You can then cancel the underlying open
or read
through ctx
Many resources will say to use O_NONBLOCK
While this may work in some cases, I actually think that blocking is the right thing to do in many cases. For example, you might not be able to otherwise synchronize with the writer, and then you will get an EOF if you try to read before the writer opens it. You can work around that by also opening it for write yourself, but that that's a lot of steps for what should be simple.
This is why it was designed to block... It is unfortunate that go does not provide a mechanism to interrupt the open directly. The runtime manages signal handling and does not let you disable SA_RESTART, and even if it did, then os.Open
will suppress EINTR
. After working around that, you'd probably be better off implementing the open in C through CGO...
Upvotes: 0
Reputation: 403
You can open FIFO in non-blocking way, according to documentation. So something like this should work:
go func() {
pipeReader, _ := os.OpenFile(pipePath, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
go poll(pipeReader) // Reads and process each entry until pipeReader is closed.
<-doneChan
pipeReader.Close()
}
Upvotes: 1
Reputation: 4000
Ugh, it took just a bit more coffee and thinking.
I forgot that I can open the other end of the pipe myself, if I know the program I executed didn't open it. And this unblocks the open for reading.
The revised code, in case anyone bumps into this:
pipeOpenedForReading := false
var muFifo sync.Mutex{}
syscall.Mkfifo(pipePath, 0600)
go func() {
pipeReader, _ := os.Open(pipePath)
muFifo.Lock()
pipeOpenedForReading = true
muFifo.Unlock()
go poll(pipeReader) // Reads and process each entry until pipeReader is closed.
<-doneChan
pipeReader.Close()
}
go func() {
<-doneChan
muFifo.Lock()
if !pipeOpenedForReading {
// Open for writing, unblocking the os.Open for reading.
f, err = os.OpenFile(pipePath, O_WRONLY, 0600)
if err == nil {
close(f)
}
}
muFifo.Unlock
os.Remove(pipePath)
}
Upvotes: 1