Odd behavior on iOS with NIOSSHClient

I am using the code in the example NIOSSHClient in my app, to issue commands to some servers. I am intending for the application to run on macOS as well as iOS. So far so good except for one small thing.

My app works fine on macOS but on iOS, every command sent via SSH returns an exit code of 1 and essentially doesn't work. I found that if I put a small sleep in the code then it works fine. I am running the iOS variant on the iPad simulator.

The code code below is from ExecHandler.swift in the channelActive() function. It is essentially unchanged from the sample app except for dup()ing the file handles and adding the aforementioned sleep() call.

    }.takingOwnershipOfDescriptors(input: dup(0), output: dup(1)).whenComplete { result in
        switch result {
        case .success:
            // We need to exec a thing.
            let execRequest = SSHChannelRequestEvent.ExecRequest(command: self.command, wantReply: false)
            context.triggerUserOutboundEvent(execRequest).whenFailure { _ in
                context.close(promise: nil)
            }
            Thread.sleep(forTimeInterval: 0.1)  // Hmm, without this the command fails if run from iOS.
        case .failure(let error):
            context.fireErrorCaught(error)
        }

Does anyone have any ideas on this?

The most likely outcome is that you're not waiting for the response. The sleep allows the response to be generated while you're forcing the sleep. When do you tear the full client down?

With very minor changes, the SSH code I am using was taken directly from the sample app of NIOSSHClient. I have the same wait()s on channels and promises. In my case the biggest change is not getting user input for credentials on the command line (i.e. InteractivePasswordPromptDelegate). Also the sample is command-line only, whereas I incorporated that code into a proper GUI application. Kinda smells like a race condition to me.

I have yet another "solution". I have no use for using stdout and stdin so I have now eliminated the GlueHandler and use of NIOPipeBootstrap, and replaced the contents of channelActive() of ChannelDuplexHandler with the following two lines:

let execRequest = SSHChannelRequestEvent.ExecRequest(command: self.command, wantReply: true)
context.triggerUserOutboundEvent(execRequest, promise: nil)

All appears to be working normally now. I can't help wonder if there's still a gotcha lurking somewhere in there. :slight_smile:

That should work just fine. The use of stdin may well have been your issue, actually, as it could potentially be triggering a close on the channel.