Need help getting started with this library

Jun 16, 2014 at 2:59 AM
For example:

How do I instantiate a shell?

Using the following code, why does "ls" work but "cd [to some folder]" doesn't?

How do I display the typical *nix prompt? (For example, I'm connecting to a BeagleBone Black with Debian installed, so no, I don't care if you see Debian's default username and password in the code below.)

This looks like a great library, and I wouldn't mind actually writing a Code Project article or two showing how to use it, but I'm stuck on some very basic stuff!

Thanks in advance,

Marc Clifton

Here's my test code (taken from another post here):
    class Program
    {
        static void Main(string[] args)
        {
            PasswordAuthenticationMethod authMethod = new PasswordAuthenticationMethod("debian", "temppwd");
            ConnectionInfo connectionInfo = new ConnectionInfo("192.168.1.42", "debian", authMethod);

            using (var ssh = new SshClient(connectionInfo))
            {
                ssh.Connect();

                while (true)
                {
                    string line = Console.ReadLine();

                    if (String.IsNullOrEmpty(line))
                    {
                        continue;
                    }

                    var cmd = ssh.CreateCommand(line);   //  very long list
                    var asynch = cmd.BeginExecute(delegate(IAsyncResult ar)
                    {
                        // Console.WriteLine("Finished.");
                    }, null);

                    var reader = new StreamReader(cmd.OutputStream);

                    while (!asynch.IsCompleted)
                    {
                        string result = reader.ReadLine();

                        if (!String.IsNullOrEmpty(result))
                        {
                            Console.WriteLine(result);
                        }
                    }

                    cmd.EndExecute(asynch);
                }
            }
        }
    }
Jun 18, 2014 at 10:53 PM
Response to Marc Clifton - Part 1

Marc,

I didn't want your post/question/issue to hang out there for too long without someone saying something from the community. I'm far from an expert, so bear with me. You have the following questions in your post:

1. How do I instantiate a shell?

There are probably more ways, but i have found/used 2 of them to make things happen in my code with Ssh.Net, and one of them (the first I used) is with a shell. In the above, you are not using a shell, I think. Here is how the docs say:
public ShellStream CreateShellStream(
    string terminalName,
    uint columns,
    uint rows,
    uint width,
    uint height,
    int bufferSize,
    IDictionary<TerminalModes, uint> terminalModeValues
)
I use this way when I have to enter a command, wait for the response, then do another command based on that response-all in the same session, which it looks like you were trying to do in your post, creating an interactive shell, of sorts(?). My implementation is a bit involved, but it boils down to:

Private ss As Renci.SshNet.ShellStream
Public Property Client As Renci.SshNet.SshClient
Private _shStream As Renci.SshNet.ShellStream
    Public Property ShStream As Renci.SshNet.ShellStream Implements IASASecureComms.ShStream
        Get
            If Me.ss Is Nothing Then
                _shStream = Me.GetShellStream
            End If
            Return _shStream
        End Get
        Set(value As Renci.SshNet.ShellStream)
            _shStream = value
        End Set
    End Property

    Public Function GetShellStream() As Renci.SshNet.ShellStream
        Try
            If Me.Client Is Nothing Then
                log.Error("GetShellStream() given an uninstantiated SshClient!")
                Return Nothing
            End If
            '
            ' are we at least connected?
            '
            If Not Me.Client.IsConnected Then
                log.Error("GetShellStream() trying to connect client...")
                Me.Client.Connect()
                log.Error("GetShellStream() could we connect? " & Client.IsConnected)
            End If
            '
            ' only create ShellStream if connected
            '
            If Me.Client.IsConnected And ss Is Nothing Then ' class level variable XXX change to param, so we can auto-instantiate in Get()
                Me.ss = Me.Client.CreateShellStream("dumb", 80, 24, 800, 600, 102400)
            ElseIf Me.Client.IsConnected And Not ss Is Nothing Then
                ' all is well! We are both connected, and ss is instantiated!
                log.Debug("GetShellStream() all is well, nothing to do here...")
            Else
                log.Fatal("GetShellStream() Client is not connected!")
                Throw New Exception("GetShellStream() Client is not connected!")
            End If
        Catch ex As Exception
            log.Fatal("GetShellStream() cannot create new ShellStream: ", ex.ToString)
        End Try
        Return ss
    End Function
... as you can see from this line:
Me.ss = Me.Client.CreateShellStream("dumb", 80, 24, 800, 600, 102400)
...I'm literally creating a shell, complete with height, width, and a buffer. Here is how I would use it for small, interactive exchanges (by small, I mean no more than 1/2 the buffer (no idea why)):

NOTE: For the purists out there, I am trying to both cleanse the code pasted, but also not give fragments, if I can get away with it. I get very frustrated with fragments because they never work as-is, and not everybody already understands the missing bits... :)
Public Function GetRunningConfig() As String
    Dim sb As StringBuilder = Nothing
    Dim line As String = ""
    Try
        Dim reader = New StreamReader(Me.ss)
        Dim writer = New StreamWriter(Me.ss)
        writer.AutoFlush = True
        sb = New StringBuilder(System.String.Empty)
        ss.Expect(New Regex(":.*>#"), TimeSpan.FromSeconds(3))
        ss.WriteLine("terminal pager 0")
        Thread.Sleep(1000)
        While ss.DataAvailable = False
            Thread.Sleep(500)
            Debug.WriteLine("No data available")
        End While
        ss.WriteLine("show running-config")
        Thread.Sleep(1000)
        While ss.DataAvailable = False
            Thread.Sleep(500)
            Debug.WriteLine("No data available")
        End While
ADDMORE:
        line = ss.ReadLine(TimeSpan.FromSeconds(3))
        If Not IsNothing(line) Then
            If line.Length > 0 Then
                'Debug.WriteLine("adding to sb: [" & line & "]")
                sb.AppendLine(line)
                GoTo ADDMORE
            Else
                Debug.WriteLine("no more!")
            End If
        End If
        Debug.WriteLine("result: [" & sb.ToString() & "]")
    Catch ex As Exception
        log.Fatal("GetRunningConfig() failed: " & ex.ToString())
    End Try
    Return sb.ToString()
End Function
One of the things here that you should note is this line:
ss.Expect(New Regex(":.*>#"), TimeSpan.FromSeconds(3))
...which tells the SshNet what to look for to tell it is is done...
You see, SshNet has no way of knowing whether your server is still thinking or not. In fact, it's not sure what the 'prompt' looks like until you tell it what to 'expect'. That is all I was doing. The above is probably a horrible example to experts, but it took me days to get it to work consistently. I spent a lot of time in the source code, to be honest. :) As you can tell, this function just grabs a running config off a router.

Hopefully you can see that the following snippet from the above is simply waiting until ss.DataAvailable is False (meaning "we're all done!" for small commands/output), waiting a little bit (I ran into trouble when doing it rapid-fire), and saving the response:
While ss.DataAvailable = False
    Thread.Sleep(500)
    Debug.WriteLine("No data available")
End While
ss.WriteLine("show running-config")
Thread.Sleep(1000)
While ss.DataAvailable = False
    Thread.Sleep(500)
    Debug.WriteLine("No data available")
End While
Below further I'll paste in a Function() that logs you in enable, as another example of using the ShellStream.
Friend Function LoginEnable() As Boolean 
    Dim r As String = "" '  for 'response' from FW
    log.Debug("===== STARTING LoginEnable()")
    Try
        If Not Client.IsConnected Then
            Me.Client.Connect()
            Me.ss = GetShellStream()
            log.Debug("LoginEnable() connected? " & Me.Client.IsConnected)
        End If
        If Me.Client.IsConnected Then
            Me.LoggedInUser = True
        End If
        '------------------------------------------------------------------
        Me.ss.Flush()
        Dim retriesEnable As Integer = 3
        While (Not r.ToLower.Contains("password")) And (retriesEnable > 0)
            r = SendCommand("enable", "LF")
            log.Debug("LoginEnable(): r1 = [" & r & "]")
            Thread.Sleep(1500)
            retriesEnable -= 1 ' count down...
        End While
        '------------------------------------------------------------------
        Me.ss.Flush()
        r = SendCommand(Me.EnablePass)
        log.Debug("LoginEnable(): r2 = [" & r & "]")
        Thread.Sleep(1500)
        If DoesContain(r, "#") Then
            Me.LoggedInEnable = True
            Me.LoggedInUser = True
            log.Info("LoginEnable(): Logged in with enable!")
        Else
            Me.LoggedInEnable = False
            Throw New Exception("LoginEnable(): enable attempt FAILED! " & r)
        End If
        Catch ex As Exception
            log.Fatal("LoginEnable(): Cannot log into enable! " & ex.ToString)
            Me.LoggedInEnable = False
        End Try
        log.Debug("===== ENDING LoginEnable() with: " & Me.LoggedInEnable)
        Return Me.LoggedInEnable
    End Function
Now I'm sure the Function SendCommand() caught your eye... That is a very long Function I wrote to allow any part of my code to simply send a command at any time. It does things like check to mae sure we're logged on (if not, log on), logged in enable (if not, log in enable), make sure we even have a ShellStream (if not, go make one), etc. You get the idea.

I also have it do 'retry' where I catch the exceptions thrown, and analyze them. I might make a change in the SshNet parameter, or connect here or there, then try it again (up to X times, usually 3). I also had to allow the code to decide what type of line ending to use (none, LF, CR, CRLF), as devices use these in an inconsistent way. I noticed the same errors were happening (you know, like after a connection times out, etc.), and I wanted to remove the logic to manage these exceptions from every part of my biz logic that wanted to send a command (a lot of dupes doing it that way!).

My post is limited to 10k, so this will be Part 1.

pat
:(
Marked as answer by drieseng on 6/22/2014 at 3:00 AM
Jun 18, 2014 at 11:01 PM
Edited Jun 18, 2014 at 11:09 PM
Response to Marc Clifton - Part 2

Marc,

...continuing on, I think I beat your #1 question to death... Let's keep things moving... :)

The next question I think I know the answer to:

2. Using the following code, why does "ls" work but "cd [to some folder]" doesn't?

Simple Answer: Because you were not creating an interactive shell. The end of the first command signaled the end of the session, and the (internal) shellstream disconnected. If you cd'd first, the opposite would happen (I couldn't see your ls or cd commands in your paste, BTW)...

This, referring to my Part 1 response, is the 2nd way I know how to interact with servers. I just figured out my first rough draft of how this woks yesterday via this post. I won't re-paste the code from that post, but I think I explained it pretty well for someone not used to using all of this... :)

_3. How do I display the typical *nix prompt? (For example, I'm connecting to a BeagleBone Black with Debian installed, so no, I don't care if you see Debian's default username and password in the code below.)_

Does that by chance use a BeagleBoard? My last company was putting their Linux server/appliance on one of those bad boys... Great developers, by the way, nice folks.

4. This looks like a great library, and I wouldn't mind actually writing a Code Project article or two showing how to use it, but I'm stuck on some very basic stuff!

How do you even write a CP article? I looked around and couldn't figure out how to even submit one for review! :)

OK. So between these 2 methods, which both work, let the community know if you aren't able to make any progress! :)

Thanks!

pat
:)


P.S. This is most of the meaningful bits of SendCommand, ignoring all the reconnect, retry, are we enable? logic...:

note: SleepMS can be adjusted dynamically, as well, by the program to suit the nature of the network, command, etc. Hard-coding timeouts based on my zippy test machines & LAN never works out in the real word! :)
[...]
'========================================================
'
' reader:
'
reader = New StreamReader(Me.ss)
'
' writer:
'
writer = New StreamWriter(Me.ss)
' writer.Flush() ' causing our Client to disconnect, for some reason...(?)
'
writer.AutoFlush = True
'
result = New StringBuilder(System.String.Empty)
'
Select Case LCase(eol)
    Case ""
        log.Debug("SendCommand() doing WriteLine(" & cmd & ")")
        writer.WriteLine(cmd)
    Case "w"
        log.Debug("SendCommand() doing Write(" & cmd & ")")
        writer.Write(cmd)
    Case "cr"
        log.Debug("SendCommand() doing Write(" & cmd & " & vbCr)")
        writer.Write(cmd & vbCr)
    Case "lf"
        log.Debug("SendCommand() doing Write(" & cmd & " & vbLF)")
        writer.Write(cmd & vbLf)
    Case Else
        log.Error("SendCommand(" & cmd & ") unknown eol: [" & eol & "]")
End Select
'
log.Debug("SendCommand() sleeping for " & sleepMs & " milliseconds (after write, before listen)")
Thread.Sleep(sleepMs)
'
Dim waitCycles As Integer = 5
While (Me.ss.DataAvailable = False) And (Me.ss.Length = 0) And (waitCycles > 0)
    log.Debug("[" & waitCycles & "] ss.Length: " & Me.ss.Length)
    Thread.Sleep(sleepMs)
    waitCycles -= 1
End While
result.AppendLine(reader.ReadToEnd())
[...]
...you get the idea!

A lot of hard hours of hair pulling in these 2 parts, but I'm glad to give back... :)
Marked as answer by drieseng on 6/22/2014 at 3:00 AM