I notices NIOSSH today for SSH support in Swift. I'm going to try it as a management console for one of my HTTP backends. For an idea of how much effort is involved I'll give a running tally here.
Go look at https://github.com/apple/swift-nio-ssh. Read a lot of it.
Discover that there is already an example server hiding in Sources/NIOSSHServer
Copy it in, drop out the code for port forwarding
I can connect to my server, and it crashes. Progress.
Realize the sample server is trying to use
/usr/local/bin/bashwhich I don't have.
Get rid of all the pipe code since I'm not going to be interactive, at least yet. Introduce a bunch of bugs. Get lost in event loops, futures, and promises and the mysteries of
- Success! I have built a really complicated
Much better understanding of NIO ChannelHandler, threw out about half the previous code.
Got in a fight with Swift over how to say a parameter conforms to a protocol. Ended up with an ugly generic solution. Not happy.
Boiled down to a SSHCommandHandler class which is used as a channel handler. It gets a command and dispatches it to a
doCommand(command:to:environment:)function which you override.
The Protocol fight was because I just want that
to:parameter to be a
TextOutputStream. I don't want to tell you about my specific version. That collides with the inout nature of the print(_:to:inout TextOutputStream) call.
Mostly liking the shape of the code. In your
main or somewhere likely you will…
let console = SSHConsole(passwordDelegate: Authenticator()) try console.listen()
…later you can…
And just like that I hit a wall. Swift Argument Parser does not have a mechanism for you to pass in state as you dispatch the command. There is a
run() with no arguments and that's it. I spent 4 hours exploring the vast array of things which don't work. I landed on a few that do, but they are a bit gross.
Here's what I ended up with:
Define a struct for my context.
ParsableCommandto require a context variable of my type. I called it
I define my commands with structs conforming to
ConsoleCommand. Each one has to have a
var context:ConsoleContext? = nilin it.
run()can look at its
contextand everything is happy
Optional<Foo>only conforms if
Fooconforms, and my context object can't conform because there is no sane null value for an output stream!
The textbook solution is to specify my own Codable keys and leave out the trouble maker, except these objects are full of variables because they have entries for all the command syntax elements.
… hours are wasted …
I finally implemented a
@transientproperty wrapper for nil-able values which sets them to nil on a
decodeand doesn't write on an
encode. This is inspired by a feature rejected by the Swift designers.
I don't like it. I resent having to put a
@transient var context:ConsoleContext?=nilin every command, but when I tried using
classes instead of
structsfor the commands it didn't parse, so that may not be supported, so no inheriting my boilerplate.
I may go back and make my context be
Decodableeven if I have to make some insane values to populate it. Then I can lose the property wrapper and also save some unwrapping.
All in all a typical day of programming. Lot's of good progress then I step in a bear trap and have to chew my leg off before continuing.
All wrapped up and published on GitHub with API level documentation and very little high level documentation. Just the way I hate to use software. You can find it at SSHConsole