@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
| '
| ' final string result (sResult):
| '
A sResult = sResult.Substring(indexOfLastCharOfCommand, sResult.Length - indexOfLastCharOfCommand)
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.
That is why you see this above:
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.
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.