Quantcast
Channel: sshnet Discussions Rss Feed
Viewing all articles
Browse latest Browse all 1729

New Post: Dealing with "prompts"

$
0
0
@Festive,

I no longer do things this way, because on the systems I am programming against today (Cisco switches, routers, & ASAs), the command prompt is the last thing output after a command. You make it sound like the return string(s) are asynchronous, and the ssh term will see the command prompt returned before the last of the response from the host.

That said, you can monitor text as it is returned, and simply RegEx or .Contains() each batch as they are added into the response as a whole. I have written a 'chunking' wrapper for my commands, as the hosts I'm programming against have a 1kB limit on the command length, and I may need to give 1000's of lines of commands in a single 'action', or 'batch'. My code got complicated quickly as the Cisco equipment will allow multiple sessions from the same user, but the client would not allow multiple SSH/22 connections to the same host-and my program is very multi-threaded.

One thing nobody on SO could solve was how to have multiple threads submit 'work', or 'jobs' as ssh commands, process each serially (one at a time), then (hard part) return the results to each calling thread. That took me a while to get right, and we'll talk about that another day! :)

So let me start with the smallest denominator in my rather complicated system (intuitively called, "SendBigCommand"):

Note: You can convert easily, but VB proves much easier to use and troubleshoot than C#, and the event/regex processing makes it easy to return to your code months later when it is unrecognizable).

[Yes, I write in both, and Java, and Perl, and... so no nasty comments; Just using the right tool for the job]

Public Function SendBigCommand(TheCommands As String, Optional IdleTimeout As Integer = 4) As String
        Dim tsIdleTimeout As TimeSpan = TimeSpan.FromSeconds(IdleTimeout)
        Dim swIdleTimeout As New Stopwatch
        Dim sbResult As New StringBuilder
        Dim sResult As String = ""
        '
        Try
            Using c As SshClient = New SshClient(Me.Host, Me.User, Me.Pass)
                c.Connect() ' only ONE thread can connect at a time!
                If c.IsConnected Then
                    '
                    ' since each command starts from scratch, we need to always get to #enable before we enter the submitted commands...
                    '
                    Dim sGetToEnable As String = "enable" & vbLf & Me.EnablePass & vbLf & "term pager 0" & vbLf
B Dim cmdLastLine As String = Me.GetLastLine(TheCommands)
                    '
                    Dim cmdCombined As String = String.Concat(sGetToEnable, TheCommands & vbLf)
                    log.Debug("cmdCombined: [" & cmdCombined & "]")
                    '
                    Using cmd As SshCommand = c.CreateCommand(cmdCombined)
                        Dim cmdIndex = cmdCombined.IndexOf(cmdLastLine)
                        Dim asynch As System.IAsyncResult = cmd.BeginExecute()
                        Using reader As StreamReader = New StreamReader(cmd.OutputStream)
                            Dim res As String = ""
                            swIdleTimeout.Start()
                            Do While Not asynch.IsCompleted
                                res = reader.ReadToEnd()
                                If String.IsNullOrEmpty(res) Then
                                    If swIdleTimeout.Elapsed.TotalSeconds >= tsIdleTimeout.TotalSeconds Then
                                        log.Debug(String.Format("Exiting from async execution.. idle for {0} seconds", swIdleTimeout.Elapsed.TotalSeconds.ToString("0.00")))
                                        ' cmd.CancelAsync() ' turns asynch.IsCompleted to True; same as doing: (not really, throws exception)
                                        Exit Do
                                    End If
                                    Continue Do
                                Else
                                    sbResult.Append(res)
                                    swIdleTimeout.Restart() ' every time we get something, we re-start the timer...
                                End If
                            Loop
                            ' here after IdleTimeout _or_ rare asynch.IsCompleted()
                        End Using
                        '
                        '=========================================================
                        '
                        ' return anything after the last line of the command (cmdLastLine)
                        '
                        sResult = sbResult.ToString ' convert SB into string
                        log.Debug("sResult: Before [" & sResult & "]")
                        log.Debug("cmdLastLine: " & cmdLastLine)
                        log.Debug("cmdLastLine.Length: " & cmdLastLine.Length)
                        '
                        ' try to remove everything up to the actual response...
                        '
A Dim indexOfLastCharOfCommand As Integer = sResult.IndexOf(cmdLastLine) + cmdLastLine.Length
| '
| ' final string result (sResult):
| '
A sResult = sResult.Substring(indexOfLastCharOfCommand, sResult.Length - indexOfLastCharOfCommand)
                        log.Debug("sResult: After [" & sResult & "]")
                        '
                    End Using
                Else
                    log.Fatal("client could not connect!")
                    Throw New Exception("Client could not connect")
                End If
            End Using
        Catch ex As Exception
            log.Fatal("EXCEPTION(M): " & ex.Message)
            log.Fatal("EXCEPTION(T): " & ex.ToString)
            log.Fatal("EXCEPTION(S): " & ex.StackTrace)
        End Try
        Return sResult
    End Function
I hope this isn't too intimidating. It evolved over time. :)

The observant will notice that (1) I'm not concerning myself with a prompt, and (2) the line: _" ' here after IdleTimeout or rare asynch.IsCompleted()"_ This is because in my situation, I simply NEVER get a signal asynch.IsCompleted-never have. I gave up and moved on by allowing a integer timeout (ToSeconds) that the calling function can set (big commands don't take a lot more than small ones, as it is just idle timeout, not whole operation timeout).

Let em explain the parts that aren't included in the function. The first is GetLastLine(), (B) which I use to trim the response (in my case, the responses include the commands as entered, which is why you see the lines at the A-A. I don't want to process the commands, just the results.
        Public Function GetLastLine(SomeLines As String) As String 
            Dim linesNL() As String
            linesNL = SomeLines.Split(CChar(Environment.NewLine))
            Dim linesLF() As String
            linesLF = SomeLines.Split(CChar(vbLf))
            Dim linesCR() As String
            linesCR = SomeLines.Split(CChar(vbCr))
            Dim linesCRLF() As String
            linesCRLF = SomeLines.Split(CChar(vbCrLf))
            ' each is an array:
            Dim linesArrayArray As String()() = {linesNL, linesLF, linesCR, linesCRLF}

            Dim largest As Integer = linesArrayArray.Max(Function(ar) ar.Count)
            log.DebugEx("largest: {0}", largest)
            ' now which array had than many lines?

            Dim descArray = linesArrayArray.OrderByDescending(Function(c) c.Count)
            log.DebugEx("descArray(0).Count is {0}", descArray(0).Count)
            log.DebugEx("descArray(1).Count is {0}", descArray(1).Count)
            log.DebugEx("descArray(2).Count is {0}", descArray(2).Count)
            log.DebugEx("descArray(3).Count is {0}", descArray(3).Count)

            Dim longestArray As String() = {}

            'For Each element As Integer In numLines
            '    large1 = Math.Max(largest, element)
            'Next

            Dim lastLine As Integer = descArray(0).Count
            Return descArray(0)(lastLine - 1)
        End Function
Now before you guys start laughing, this is how I solved the problem of not knowing whether the commands are given (or even mixed) as nothing, LF, CR, or CRLF at the end of each line. Welcome to Cisco IOS...

That is why you see this above:
Dim linesArrayArray As String()() = {linesNL, linesLF, linesCR, linesCRLF}
You find a better way, I'm all ears!

Suggestion

In the above SendCommand, you will see the following lines:
[...]
Else
    sbResult.Append(res)
    swIdleTimeout.Restart() ' every time we get something, we re-start the timer...
End If
[...]
Every time I am there, it means that a line of test/string (var res) has gobbled up some response. Becuase of this activity, the command swIdleTimeout.Restart() does what it says, and resets the idle timer.

So this is the perfect place to do regex on the last text received from the host (might be multi-line, so prepare for that?), using the string variable 'res' (in my case). If you regex res, and you see a match for your command prompt, or whatever, then you can trigger another action, flip a boolean and wait, or even redirect the output, etc.

Knowing what little I do about your issue, and selecting only fro the code I have written, this seems a good mechanism to inspect the host response as it is coming back to the client. Now be mindful, your host might return ALL of your data from a command in one big string, or it may give it to you line-by-line... Ebanle lots of debugging to see.

Note On Code: I tried to remove as much comments/etc. for clarity.

Viewing all articles
Browse latest Browse all 1729

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>