Jim's Depository

this code is not yet written
 

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.

T+0

  • 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

T+1hr

  • I can connect to my server, and it crashes. Progress.

  • Realize the sample server is trying to use /usr/local/bin/bash which 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 unwrapInboundIn and friends.`

T+2hr

  • Success! I have built a really complicated echo server.

T+4hr

  • 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.

T+6hr

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…

try? console.stop().wait()`

T+10hr

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.

  • Extend the ParsableCommand to require a context variable of my type. I called it ConsoleCommand

  • I define my commands with structs conforming to ConsoleCommand. Each one has to have a var context:ConsoleContext? = nil in it.

  • Now my run() can look at its context and everything is happy

  • Except… ParsableCommand conforms to Decodable and an Optional<Foo> only conforms if Foo conforms, 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 @transient property wrapper for nil-able values which sets them to nil on a decode and 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?=nil in every command, but when I tried using classes instead of structs for 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 Decodable even 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.

T+20 Hours

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