0

I have a Windows Service that is going to process results from plink.exe (Putty/SSH thing). I'm currently successfully capturing the standard output and sending commands via the standard input to the process.

My problem is, the OutputDataReceived event doesn't seem to get raised until I receive a line break from the console application process. The process prompts for a password and there isn't a line break until after I input a password.

So my question is, is there a way to process the Standard Output character-by-character, instead of line-by-line from the System.Diagnostics.Process class?

Here is my code:

_processInfoTest = new ProcessStartInfo();
_processInfoTest.FileName = serviceSettings.PlinkExecutable;
_processInfoTest.Arguments = GetPlinkArguments(serviceSettings);
_processInfoTest.RedirectStandardOutput = true;
_processInfoTest.RedirectStandardError = true;
_processInfoTest.RedirectStandardInput = true;
_processInfoTest.UseShellExecute = false;
_processInfoTest.CreateNoWindow = true;

_processTest = new Process();
_processTest.StartInfo = _processInfoTest;
_processTest.OutputDataReceived += processTest_OutputDataReceived;
_processTest.ErrorDataReceived += processTest_OutputDataReceived;
_processTest.Start();

_processTest.BeginOutputReadLine();
_processTest.BeginErrorReadLine();

And the event handler that handles the incoming lines of text:

private static void processTest_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
    string line = e.Data;

    if (line != null)
    {
        WcfServerHelper.BroadcastRemoteCallback(x => x.PlinkTextOutput(line, DateTime.Now));

        if (line.Contains("If you do not trust this host, press Return to abandon the"))
        {
            _processTest.StandardInput.Write("y");
            _processTest.StandardInput.Write("\n");
            _processTest.StandardInput.Flush();
        }

        // This code never gets called because the event doesn't get raised until a line-break occurs
        if (line.Contains("'s password:"))
        {
            _processTest.StandardInput.Write("mypassword");
            _processTest.StandardInput.Write("\n");
            _processTest.StandardInput.Flush();
        }

        if (line.Contains("Access granted"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.Success));
        }
        else if (line.Contains("Access denied") || line.Contains("Password authentication failed"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.InvalidUserOrPass));
        }
        else if (line.Contains("Host does not exist"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.InvalidHostname));
        }
        else if (line.Contains("Connection timed out"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.TimedOut));
        }
    }
}

Thank you!

Adam Plocher
  • 13,994
  • 6
  • 46
  • 79

2 Answers2

1

the event you are using is line buffered ... you will have to implement your own reader that calls Read() / ReadAsync() on the streamreader to get every char ...

DarkSquirrel42
  • 10,167
  • 3
  • 20
  • 31
1

I was looking for an answer forever, and right after asking the question here I found my solution.

Btw, thanks DarkSquirrel, you hit the nail on the head.

Here is my solution:

_processInfoTest = new ProcessStartInfo();
_processInfoTest.FileName = serviceSettings.PlinkExecutable;
_processInfoTest.Arguments = GetPlinkArguments(serviceSettings);
_processInfoTest.RedirectStandardOutput = true;
_processInfoTest.RedirectStandardError = true;
_processInfoTest.RedirectStandardInput = true;
_processInfoTest.UseShellExecute = false;
_processInfoTest.CreateNoWindow = true;

WcfServerHelper.BroadcastRemoteCallback(x => x.PlinkTextOutput(_processInfoTest.Arguments, DateTime.Now));

_processTest = new Process();
_processTest.StartInfo = _processInfoTest;
_processTest.Start();

Task.Factory.StartNew(() =>
    {
        ProcessOutputCharacters(_processTest.StandardError);
    });

Task.Factory.StartNew(() =>
    {
        ProcessOutputCharacters(_processTest.StandardOutput);
    });

And my methods:

private static void ProcessOutputCharacters(StreamReader streamReader)
{
    int outputCharInt;
    char outputChar;
    string line = string.Empty;

    while (-1 != (outputCharInt = streamReader.Read()))
    {
        outputChar = (char)outputCharInt;
        if (outputChar == '\n' || outputChar == '\r')
        {
            if (line != string.Empty)
            {
                ProcessLine("Output: " + line);
            }

            line = string.Empty;
        }
        else
        {
            line += outputChar;

            if (line.Contains("login as:"))
            {
                _processTest.StandardInput.Write("myusername");
                _processTest.StandardInput.Write("\n");
                _processTest.StandardInput.Flush();
            }

            if (line.Contains("'s password:"))
            {
                _processTest.StandardInput.Write("mypassword");
                _processTest.StandardInput.Write("\n");
                _processTest.StandardInput.Flush();
            }
        }
    }
}

private static void ProcessLine(string line)
{
    if (line != null)
    {
        WcfServerHelper.BroadcastRemoteCallback(x => x.PlinkTextOutput(line, DateTime.Now));

        if (line.Contains("If you do not trust this host, press Return to abandon the"))
        {
            _processTest.StandardInput.Write("y");
            _processTest.StandardInput.Write("\n");
            _processTest.StandardInput.Flush();
        }

        if (line.Contains("Access granted"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.Success));
        }
        else if (line.Contains("Access denied") || line.Contains("Password authentication failed"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.InvalidUserOrPass));
        }
        else if (line.Contains("Host does not exist"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.InvalidHostname));
        }
        else if (line.Contains("Connection timed out"))
        {
            if (!_processTest.HasExited)
                _processTest.Kill();

            WcfServerHelper.BroadcastRemoteCallback(x => x.TestConnectionCallback(PlinkStatus.TimedOut));
        }
    }

}

My only problem now is when I send the username or password (through StandardInput), it's only sending the first 5 characters. I'll do some research on the issue and post that as a separate question if I need to.

Thanks!

Adam Plocher
  • 13,994
  • 6
  • 46
  • 79