I don't have a lot to say, but this is my little bit.

Friday, August 5, 2011

Asynchronously Read STDOUT and STDERR in SharpSSH RunCommand

SharpSSH is an SSH library for C#. It usefully comes with a smattering of classes for doing common procedures with SSH. Recently, I wrote a program which uses SharpSSH to connect to a second application tier, run one command, and read both STDOUT and STDERR.

The server tier returns a byte stream over STDOUT and prints informational, debug, and trace statements to STDERR. My desktop tier writes the byte stream (STDOUT) to a file, and displays STDERR in a text box. In this way, the user of the desktop application can watch the STDERR output from the server as it happens, while simultaneously the STDOUT byte stream goes into a file.

SharpSSH provides two implementations of SshExec.RunCommand:
  • RunCommand(string command)
  • RunCommand(string command, ref string StdOut, ref string StdErr)

The second one of those is tantalizingly close to what I need, but in fact STDOUT and STDERR are written to those strings synchronously, and the calling code can not read the values until RunCommand returns. That was unacceptable for my use. I required a new implementation of SshExec.RunCommand:
  • RunCommand(string command, Stream stdout, Stream stderr)

That would have been ideal, and would have been maximally generalized, but I took a minor shortcut because of the fact that the rest of my code uses a custom TextWriter for STDERR, so this is my implementation of SshExec.RunCommand:
  • RunCommand(string command, Stream stdout, TextWriter stderr)

The design of the method is subtle. The reads have to be done asynchronously so that reading one stream does not block reading the other one. On the other hand, the writes are done synchronously, because I had better luck debugging the code that way and making it stable. A pair of flags are used to signal when the code should exit.
public int RunCommand(string command, Stream stdoutOutput, TextWriter stderrOutput)

{
m_channel = GetChannelExec(command);
System.IO.Stream stdout = m_channel.getInputStream();
System.IO.Stream stderr = ((ChannelExec)m_channel).getErrStream();
m_channel.connect();

byte[] stdoutByteBuffer = new byte[1024];
byte[] stderrByteBuffer = new byte[32];

////////////////////////////////////////////////////////////////////////////
bool stdoutIsExhausted = false;
AsyncCallback stdoutReadCallback = null;
stdoutReadCallback = readResult =>
{
int bytesRead = stdout.EndRead(readResult);
if (bytesRead > 0)
{
stdoutOutput.Write(
stdoutByteBuffer,
0,
bytesRead
);
stdout.BeginRead(stdoutByteBuffer, 0, stdoutByteBuffer.Length, stdoutReadCallback, null);
}
else if (bytesRead < 0)
{
stdoutOutput.Flush();
stdoutIsExhausted = true;
}
};
////////////////////////////////////////////////////////////////////////////
bool stderrIsExhausted = false;
AsyncCallback stderrReadCallback = null;
stderrReadCallback = readResult =>
{
int bytesRead = stderr.EndRead(readResult);
if (bytesRead > 0)
{
string text = System.Text.ASCIIEncoding.ASCII.GetString(stderrByteBuffer, 0, bytesRead);
text = text.Replace("\n", Environment.NewLine);
stderrOutput.Write(text);
stderr.BeginRead(stderrByteBuffer, 0, stderrByteBuffer.Length, stderrReadCallback, null);
}
else if (bytesRead < 0)
{
stderrIsExhausted = true;
}
};
////////////////////////////////////////////////////////////////////////////

stdout.BeginRead(stdoutByteBuffer, 0, stdoutByteBuffer.Length, stdoutReadCallback, null);
stderr.BeginRead(stderrByteBuffer, 0, stderrByteBuffer.Length, stderrReadCallback, null);

while (!(stdoutIsExhausted && stderrIsExhausted))
{
// empty loop; wait until reads are complete before returning
}

m_channel.disconnect();
return m_channel.getExitStatus();
}


To use this code, put it next to the other RunCommand methods in SshExe.cs.

4 comments:

  1. Hmmm... at a bit of a loss as to what kind of Stream / TextWriter to pass into this new RunCommand(). Are you able to show a snippet of the calling code?

    ReplyDelete
  2. Figured it out :-) and works perfectly - thanks!

    ReplyDelete
  3. Can you please provide me an example on how do you call this

    ReplyDelete
  4. Sandeep and others - you'd call it like this to get the result as strings. If you want to use the streams as Nicholas did then just access them directly.

    string returnedStdout, returnedStderr;

    MemoryStream stdoutStream = new MemoryStream();
    MemoryStream stderrStream = new MemoryStream();

    exitStatus = exec.RunCommand(command, stdoutStream, stderrStream, true);

    returnedStdout = MemoryStreamToString(stdoutStream);
    returnedStderr = MemoryStreamToString(stderrStream);

    stdoutStream.Close();
    stderrStream.Close();

    ReplyDelete