Glad we got this fixed. Yeah, I think the fd ownership issue didn't matter yet because the Pipe didn't deinit before you hit the other issue.
Probably not. As you know, channelRead delivers you the data read. But NIO can't predict how much data is available in the kernel, so SwiftNIO may need to read it in multiple attempts. channelReadComplete marks the end of one of those "read bursts". So you'll need to expect many channelReadCompletes.
You could implement
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
defer {
context.fireUserInboundEventTriggered(event)
}
if case .inputClosed = event as? ChannelEvent {
// we read EOF, ie. the input is now closed
self.inputClosed = true
}
}
in your ChannelInboundHandler/ChannelDuplexHandler, that way you can learn that your input was closed.
Probably details but likely you'll want context.close(promise: nil) here (which implies mode: .all). I'd only use context.close(mode: .output) if you really want to keep the input alive. Ie. context.close(mode: .output) only makes sense if you really need to send an EOF to the other side without also closing your input.
Ugh, are you adding the handler in the channelInitializer? If yes, this could be a bug in NIO, do you have a repro for this?
I'd recommend against switching autoRead off. It's almost certainly not going to do what you want. For all stream oriented transports like UNIX pipes and TCP, you cannot rely the framing of your data, so assuming the number of reads you'll need to read all the data won't really work.
Digression: framing, feel free to skip if you know that already...
In other words, say one end sends "hello world", there are no guarantees that you will read this as "hello world". You may read it for example as "hello" followed by a " world". And in the same way, if the other end sends "hello world" followed by "it's cold today". You may read it as "hello worldit's cold today". There's nothing SwiftNIO can do here, that's just how stream-oriented transports work.
To fix this, most (network) protocols implement some form of framing. In the UNIX context, "line framing" is often used. Ie. we process one line at a time. So if you send "hello world\n" followed by "it's cold today\n", the other side may receive that as "hello world\nit's cold today\n" but it can still restore the original framing. Other, more common ways of framing is to prepend a length before each message. SwiftNIO works really great with this because you can add a ChannelHandler that takes care of the framing and after that, your pipeline works on messages and no longer on a stream of bytes. So you can totally forget about the fact that you're on a stream-based transport. In swift-nio-extras we have for example LineBasedFrameDecoder which can "decode" newline framed messages. Similarly LengthFieldBasedFrameDecoder which does the same for length-prefixed messages.
There are multiple reasons why I'd recommend against autoRead = false but the strongest in your case is that you'd then rely on all of your data arriving in a single read. This may work in your testing but may well fail at a random point in time, you can't control how the kernel (and the other end) work.