I'm creating a C++ class for executing SSH commands, using libssh2.
The life cycle of a libssh2 SSH session goes through these stages:
- Initialization (acquires local resources)
- Handshake/Authentication (establishes an SSH session on the remote host)
- Disconnect (terminates SSH session on the remote host)
- Free (releases local resources; if necessary, also performs step 3).
Before step 1, we have to open a socket, which we pass to libssh2 in step 2. From then on, we don't need to pass the socket anymore, as libssh2 will store a reference to it. After step 4, we can close the socket.
I'm exposing this through a class (SSHSession
), and I'd like setup (steps 1 and 2) to happen on ctor and teardown (steps 3 and 4) to happen on dtor (since step 2 is time-consuming, this will allow me to keep a pool of sessions open and reuse it to execute commands).
My first attempt concentrated all the code on SSHSession
, and its ctor quickly became a mess, with the "if this step fails, then we must see what has already been done and undo it" routine; the dtor was not as complex, but I still found it too "busy".
Then, I divided the work across several classes, implementing RAII for each acquire/release step, namely:
- Steps 1 and 4.
- Steps 2 and 3.
I created a class SessionConnection
that implemented steps 2 and 3, and had a member of type SessionHandle
that implemented steps 1 and 4; it also had the socket as a data member, creating the first order dependency on ctor/dtor - the socket could not be destroyed before the SessionHandle
member.
As I was considering my design, I figured I could arrange steps 2 and 3 like this:
2.1. Handshake (establishes an SSH session on the remote host)
2.2. Authentication
3. Disconnect (terminates SSH session on the remote host)
Which means I could further simplify my SessionConnection
class, implementing another class to perform RAII on steps 2.1 and 3, and ending up with something like this:
- Class
SSHSession
has aSessionConnection
data member. - Class
SessionConnection
implements step 2.2. SessionConnection
has a socket data member.SessionConnection
has aSessionHandle
data member, that implements steps 1 and 4. This must be destroyed before the socket.SessionConnection
has aRemoteSessionHandle
data member, that implements steps 2.1 and 3. This must be created after, and destroyed before, theSessionHandle
data member.
This greatly simplifies my ctors/dtors, courtesy of RAII. And I find conceptually sound; if, on the one hand, we can look at these as states the SSH session goes through, on the other hand, we can also see them as resources (local, remote) we're managing.
However, I now have a strict construction/destruction order in SessionConnection
, and while I believe it's an improvement (and I found nothing in my research that stated "this is evil", only "this should be clearly documented"), I'm interested in other opinions, and I'll happily accept info/pointers about possible alternatives to this design.
What I've looked at, so far:
- ScopeGuard, ScopeExit. It's a similar mechanism, I didn't find anything I could use as an improvement on my design.
- State Machine. I couldn't find a way to simplify the design with it, and it didn't solve one problem RAII does, namely, cleanup if something fails.
Thanks for your time.