Saturday 17 November 2012

Success with the new design! And RemoteSessionHandle

It's still early days, there's still a lot to polish, but it connects and disconnects successfully.

On this post, I'll introduce RemoteSessionHandle, the class responsible for implementing RAII on the SSH session remote "resources", i.e., the SSH session on the remote host.

Once again, the interface is simple:

class RemoteSessionHandle
{
public:
    RemoteSessionHandle(boost::function<void(std::string)>& pReportStatus,  
        boost::asio::ip::tcp::socket& pSock, SessionHandle& pSessionHandle);
    ~RemoteSessionHandle();
 

    void CloseSession();
 

    // WE MAY ADD MOVE IN THE FUTURE, BUT NOT COPY
    RemoteSessionHandle(RemoteSessionHandle&&) = delete;
    RemoteSessionHandle& operator=(RemoteSessionHandle&&) = delete;
    RemoteSessionHandle(RemoteSessionHandle const&) = delete;
    RemoteSessionHandle& operator=(RemoteSessionHandle const&) = delete;
private:
    utils::TaskState DoHandshake();
    utils::TaskState DoCloseSession();
 

    utils::CleanupState cleanupState;
    boost::function<void(std::string)>& reportStatus;
    boost::asio::ip::tcp::socket& sock;
    SessionHandle& sessionHandle;
};


Ctor, dtor, and a method to explicitly close the session on the remote host. The private methods are the handlers for the loop functions.

We're storing references to the report callback (to let the UI know what we're doing), to the socket (used in the ctor to create the session), and to an instance of SessionHandle (because the libssh2 calls need the LIBSSH2_SESSION* stored in SessionHandle).

RemoteSessionHandle::RemoteSessionHandle( 
    boost::function<void(std::string)>& pReportStatus,
    boost::asio::ip::tcp::socket& pSock, SessionHandle& pSessionHandle) :
    cleanupState(utils::CleanupState::NoCleanup), 
    reportStatus(pReportStatus), sock(pSock), sessionHandle(pSessionHandle)
{
    if (!reportStatus.empty())
        reportStatus("SSH Handshake");
 

    io_service& ios = sock.get_io_service();
    ios.reset();
    sock.async_read_some(boost::asio::null_buffers(), bind(AsyncLoopSocket,
        protect(bind(&RemoteSessionHandle::DoHandshake, this)), 
        boost::ref(sock), true));
    ios.run();
}


The ctor contains the SSH handshake. We get the io_service from the socket, and use it to drive our loop.

RemoteSessionHandle::~RemoteSessionHandle()
{
    if (!reportStatus.empty())
        reportStatus("Cleanup: SSH remote session.");
 

    // WE'LL ONLY TRY TO CLEANUP IF NO ATTEMPT HAS BEEN MADE YET
    if (cleanupState == utils::CleanupState::NoCleanup)
    {
        try
        {
            CloseSession();
        }
        catch (...)
        { }
    }
}


The dtor's behaviour is similar to SessionHandle's dtor. We only perform the cleanup if no attempt has been previously made.

void RemoteSessionHandle::CloseSession()
{
    io_service ios;
    deadline_timer dt(ios);
 

    dt.expires_from_now(milliseconds(10));
    dt.async_wait(bind(AsyncLoopTimer, 
        protect(bind(&RemoteSessionHandle::DoCloseSession, this)), 
        boost::ref(dt), 10));
 

    cleanupState = utils::CleanupState::CleanupInProgress;
    ios.run();
 

    cleanupState = utils::CleanupState::CleanupDone;
}


CloseSession() sets up the loop with a timer, and manages the cleanup state.

utils::TaskState RemoteSessionHandle::DoHandshake()
{

    int rc = libssh2_session_handshake(sessionHandle.GetSession(),  
        sock.native_handle());
 

    if (rc == LIBSSH2_ERROR_EAGAIN)
    {
        return utils::TASK_WORKING;
    }
 

    if (rc)
    {
        BOOST_THROW_EXCEPTION(SSHHandshakeError() <<
            ssh_error_string("Error " + lexical_cast<string>(rc) +  
            " during SSH Handshake.") << ssh_error_id(rc));
    }
 

    return utils::TASK_DONE;
}
 

utils::TaskState RemoteSessionHandle::DoCloseSession()
{
    int rc = libssh2_session_disconnect(sessionHandle.GetSession(),  
        "Disconnect SSH session");
 

    if (rc == LIBSSH2_ERROR_EAGAIN)
    {
        return utils::TASK_WORKING;
    }
 

    if (rc)
    {
        BOOST_THROW_EXCEPTION(SSHDisconnectSessionError() <<
            ssh_error_string("Error " + lexical_cast<string>(rc) +  
            " disconnecting SSH session.") << ssh_error_id(rc));
    }
 

    return utils::TASK_DONE;

}


DoHandshake() and DoCloseSession() follow the same pattern. There's not much to add here.

This post is very short on words, mostly just stating the obvious. I believe that's a good thing - it means the class is simple.

The code is here.

Monday 12 November 2012

SSH Session - Class SessionHandle

And... I'm back. Work has gone back to manageable levels, and I've taken a few days off C++, to give my head a chance to catch up.

I also took the opportunity to get some feedback on my design. It was quite satisfying to hear that I've been heading in the right direction all this time. And, once again, while writing (and rewriting) that question, I've hit yet another design change, one that I believe will simplify my work when I introduce other forms of authentication (for now, it's just login/password).

So, let's get moving.

Today, I'll introduce SessionHandle, the class responsible for implementing RAII on the local SSH session resources.

The interface is pretty simple:

class SessionHandle
{
public:
    SessionHandle();
    ~SessionHandle();
 

    void CloseSession();
    LIBSSH2_SESSION* GetSession() { return session; }
 


    // WE MAY ADD MOVE IN THE FUTURE, BUT NOT COPY
    SessionHandle(SessionHandle&&) = delete;
    SessionHandle& operator=(SessionHandle&&) = delete;
    SessionHandle(SessionHandle const&) = delete;
    SessionHandle& operator=(SessionHandle const&) = delete;
 


private:    
    utils::TaskState DoCloseSession();
 


    utils::CleanupState cleanupState;
    LIBSSH2_SESSION *session;
};

We define a ctor/dtor. All other special operations are deleted. I suppose I may have to implement move semantics, but I won't worry about that now. CloseSession() allows the client to explicitly release the resources, in which case the dtor won't do anything. GetSession() is necessary for the other classes that need the LIBSSH2_SESSION*, although I may review this and make these classes friend, thus removing the need to expose it to the outside world.

DoCloseSession() is the handler called by the loop functions, and cleanupState is a flag to tells us if we need to cleanup on the dtor.

The code itself is also simple, which is good, since this was one of the goals of this redesign.

SessionHandle::SessionHandle() : 
    cleanupState(utils::CleanupState::NoCleanup), 
    session(nullptr)
{
    session = libssh2_session_init();
 

    if (!session)
    {
        BOOST_THROW_EXCEPTION(SSHCreateSessionError() << 
            ssh_error_string("Error creating SSH session."));
    }
 

    libssh2_session_set_blocking(session, 0);
}


There's no need for a loop here, because creating the session is always a blocking operation. Speaking of which, for now blocking on the session is hard-coded. I may make it configurable in a future version.

SessionHandle::~SessionHandle()
{
    // WE'LL ONLY TRY TO CLEANUP 
    // IF NO ATTEMPT HAS BEEN MADE YET
    if ((session != nullptr) && 
        (cleanupState == utils::CleanupState::NoCleanup))
    {
        try
        {
            CloseSession();
        }
        catch (...)
        { }
    }
}
 

The dtor calls CloseSession() to perform the cleanup, but only if no attempt has been previously made; meaning, if the user didn't call CloseSession() explicitly. If such an attempt was made, we won't try it again (even if it failed).

Also on the to-do list is applying boost's current_exception() to the catch block.

void SessionHandle::CloseSession()
{
    io_service ios;
    deadline_timer dt(ios);
 

    dt.expires_from_now(milliseconds(10));
    dt.async_wait(bind(AsyncLoopTimer, 
        protect(bind(&SessionHandle::DoCloseSession, this)), 
        boost::ref(dt), 10));
 

    cleanupState = utils::CleanupState::CleanupInProgress;
    ios.run();
 

    session = nullptr;
    cleanupState = utils::CleanupState::CleanupDone;
}


The use of a timer and a local io_service here avoids the whole outstanding handlers issue. Once cleanup is completed, we set the state accordingly, to make sure our dtor won't try to cleanup again.

utils::TaskState SessionHandle::DoCloseSession()
{
    int rc = libssh2_session_free(session);
 

    if (rc == LIBSSH2_ERROR_EAGAIN)
    {
        return utils::TASK_WORKING;
    }
 

    if (rc)
    {
        BOOST_THROW_EXCEPTION(SSHFreeSessionError() <<
            ssh_error_string("Error " + lexical_cast<string>(rc) + 
            " freeing SSH session"));
    }
 

    return utils::TASK_DONE;
}


Finally, the handler follows the pattern we've setup a couple of months ago. So, nothing new here.

Incidentally, I said in that post that "This is equivalent to libssh2's ssh2_exec example, and I'll be discussing it here in the following posts". I haven't discussed the example here, as such, because as I started writing the posts about it (and actually thinking about its design), I became aware of its failings, and that's when the redesign began. However, the functionality is still identical, it's only the organization that changed.

This class is the first result of this redesign. It's simple, it's focused. And I suppose I'll learn at some point in the future it's not entirely right. But that's a good thing. Otherwise, how would we learn?

You can get all the code here, namely:
  • exception.h, which contains the base exception definitions.
  • sshexception.h, which contains the exception definitions for the ssh classes.
  • misc.h, which contains the enum with our cleanup state.
  • sessionhandle.h and sessionhandle.cpp. The class we've presented above.
And you'll also need this.

Yes, I know. I need to setup a place somewhere to store this code.