Running NIO Client as framework

Hey I have a quick question. Maybe I am doing something wrong, I have implemented a NIO Client and when I run the code on in my client project everything seems to work fine, however I took the same code and created a framework and when I import the repo into my project and use it for some reason my run method doesn't seem to finish connecting to the server. When I run it as a framework I never hit .whenSuccess or .whenFailure

public func run() {
    let messageSentPromise: EventLoopPromise<Void> =      group.next().makePromise()
    let connection = clientBootstrap()
        .connect(host: host, port: port)
        .map { channel -> () in
            self.channel = channel
        }
    connection.cascadeFailure(to: messageSentPromise)
    messageSentPromise.futureResult.map {
        connection.whenSuccess {
            guard let address = self.channel?.remoteAddress else {return}
            print("Client connected to ChatServer: \(address)")
        }
    }.whenFailure { error in
        print("Client failed to run for the following reason: \(error)")
        self.shutdown()
    }
}
//My Client
    @objc func startObservingNIO() {
    nioClient = NIOClient(host: "localhost", port: 8081)
    nioClient?.run()
}

Did you maybe by accident shut down the EventLoopGroup? This often happens if the discouraged pattern of shutting down the EventLoopGroup in deinit of a class is used.

I am not shutting down the client connection inside deinit. The only place I shutdown is if the connection fails on .whenFailure.

this is how I am shutting down

    public func shutdown() {
    do {
        try group.syncShutdownGracefully()
    } catch {
        print("Could not gracefully shutdown, Forcing the exit (\(error)")
        exit(0)
    }
    print("closed server")
}

Is this the correct way to shutdown the client?

Sure, that works, assuming you don't try to shut it down too early. And given that syncShutdownGracefully only fails on programmer error, you can also just write try! group.syncShutdownGracefully() but that's a detail.

thank you for that detail, I am still not sure as to why the program won't connect while ran as a library otherwise while ran not as a library it seems to run fine.

Here is the full code

Any thoughts?

You have a connect timeout of 1 hour. Maybe you want to reduce that to something like 5 or 10 seconds?
With Network.framework, if you connect but there's no network path to the server available (yet), it waits because that network path may be available later. That however means that if you say mistyped the hostname/port or something, then it may appear to just "hang" because it's waiting for the path to the typo'd host to become available.
So yeah, reduce the connect timeout and see if you get an error then.

Also, is this on an iOS device or Simulator?

1 Like

Thank you. No It is a Mac app, so I am just running it on Xcode. I have discovered that for some reason when the server gets the object that I am sending it is an empty string so when trying to decode the logic is just returning from the block.

When this method gets called the Object isn't empty so for some reason something must be happening to it by the time it leaves the client

     public func sendEncryptedObject(chat: EncryptedObject) {
    channel?.writeAndFlush(chat, promise: nil)
    dataReceived()
}

We are actually getting a connection so that is good.

And sync shutdown seems to be throwing this error.

CartisimNIOClient failed to run for the following reason: ioOnClosedChannel

Is the problem in how I create the server bootstrap?

  let chatHandler = ChatHandler<EncryptedObject>()

    private var serverBootstrap: ServerBootstrap {
    return ServerBootstrap(group: group)
        
        .childChannelInitializer { channel in
            channel.pipeline.addHandlers([
                BackPressureHandler()
            ])
            .flatMap {
                channel.pipeline.addHandlers([
                    ByteToMessageHandler(LineBasedFrameDecoder()),
                    self.chatHandler,
                    MessageToByteHandler(JSONMessageEncoder<EncryptedObject>())
                ])
            }
        }
        .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
}

This means that you're doing an operation (presumably write) on a Channel that is already closed. If you stick a channelInactive(context: ChannelHandlerContext) in one of your channel handlers, you should be able to observe when the channel becomes inactive.

And it's expected that syncShutdownGracefully on the EventLoopGroup closes all Channels. The expectation is that you create one EventLoopGroup when your program starts, and you shut it down when your program terminates.

So the problem is that it is failing on connection when it is a library but not when it is embedded into the client. I believe I am not returning an ELF perhaps?

I have a port and host, if I do not close try and close the channel because it was previously giving isOnClosedChannel error because when it fails I call shut down, but there was never a channel to shutdown.

public func connect(host: String, port: Int) -> EventLoopFuture<Channel> {
    let validPortRange = Int(UInt16.min)...Int(UInt16.max)
    guard validPortRange.contains(port) else {
        return self.group.next().makeFailedFuture(NIOTSErrors.InvalidPort(port: port))
    }

    guard let actualPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
        return self.group.next().makeFailedFuture(NIOTSErrors.InvalidPort(port: port))
    }
    return self.connect(endpoint: NWEndpoint.hostPort(host: .init(host), port: actualPort))
}

I'm not sure I completely follow what the problem is. Are you able to create a small reproducer that you can share in a github project or so? Doesn't need to do anything real, just a demonstration of what doesn't work.

Conceptually, there's no difference to whether the code is run from the main binary or a framework, both should work just fine and we have used it from frameworks before.

Thanks for the suggestion to build a sample client project. As I did the library works fine. However in my Actual Client project it still doesn't work. I have next to strip everything in the actual project down to a simple window and a button to test if the connection will work, but it still doesn't. I am not sure if it is because the project was created with an older Xcode version from about 9 months ago. I dug through all of the project settings I believe and only really found 2 difference between the sample project and the actual project. 1) Actual project is Automatically Signed and 2) Under the Target Build Settings there is a tab that says Architecture and it says Standard Architecture (Apple Silicon, Intel) - $(ARCHS_STANDARD), when the sample project was not created with this tab it is empty. So I am left scratching my head as to with my real world project hangs when connecting but any new project I create connects with no problem.

This all shouldn't matter. Can you try the following:

  1. Start your server
  2. Run sudo tcpdump -i lo0 -w ~/Desktop/packets.pcap '' 'port 12345' (replace 12345 with your actual port number)
  3. Start your client which shows the problem (ie. doesn't connect)
  4. Press Ctrl+C in the terminal that runs the tcpdump
  5. Share packets.pcap on your Desktop

Hi @Cole, I copied your CartisimNIOClient source code into a SwiftUI app and it failed to compile with 5 errors in
@objc public class CartisimNIOClient
So I changed
private var channel: Channel? = nil
to
private var channel: NIO.Channel? = nil
and now it compiles without errors. Does this help you?

Hey, thank you for letting me know. I have been updating the package, so this helps. I will take a look at your change in the next couple days.

Much Appreciated