{line" /> {line" /> {line"/>

C# Process.StandardOutput.Peek not working

489 Views Asked by At

I need to use standard input/output on process, so I created simple app "test":

var line = String.Empty;
do
{
    Console.Write($"previous input ==> {line}, type next input> ");
    line = Console.ReadLine();
}
while (!String.IsNullOrWhiteSpace(line) && line != "quit");

Console.WriteLine("End");

which receives something on standard input and writes on output. Then I created new app which needs to start that app "test" and use standard iput/output like:

            var process = new Process
            {
                EnableRaisingEvents = false,
                StartInfo = new ProcessStartInfo
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    RedirectStandardInput = true,
                    Arguments = Arguments,
                    CreateNoWindow = true,
                    FileName = Name,
                    WindowStyle = ProcessWindowStyle.Hidden,
                    WorkingDirectory = WorkingDirectory
                },
            };

            process.Start();

            String? input;
            do
            {
                Thread.Sleep(10000); // Sleep to be sure that "test" app generated output

                var line = String.Empty;
                while (process.StandardOutput.Peek() > -1)
                    line += (char)process.StandardOutput.Read();
                

                Console.Write($"[Standard Output]{line}\t[New Input]");
                input = Console.ReadLine();
                process.StandardInput.WriteLine(input);
            }
            while (input != "quit");

The problem is that I get this as output:

[Standard Output]previous input ==> , type next input>  [New Input]test
[Standard Output]       [New Input]

The "process.StandardOutput.Peek()" second time is returning -1 and there exist output of "test" app. Is it possible to get next what is generated on standard output by "test" app from app that started that process.

I need to get second output generated from "test" app, so I expect to see line:

[Standard Output]previous input ==> test, type next input>  [New Input]
3

There are 3 best solutions below

0
Jerko Sladoljev On BEST ANSWER

After trying a lot of things it seams that Process.StandardOutput.Peek is not working. You can use the "PeekNamedPipe" (answer from radian) for Windows OS. I managed to work it on Windows and Linux using CliWrap (https://github.com/Tyrrrz/CliWrap). The problem here is that for input stream you need Stream that has blocking read() method, so I creted/implemented one for me. I will not put here the implementation of that stream, you can use any stream that satisfy that condition. So, here is the final version of above example using CliWrap library

            var stdOutBuffer = new StringBuilder();
            var stdErrBuffer = new StringBuilder();

            // The CliStream is my own implementation of Stream class.
            // The CliWrap library is calling method: int Read(byte[] buffer, int offset, int count)
            //  with parameters offset = 0 and count = 131072
            //  it is important that this method is blocking if nothing is in stream (it done it using Semaphores)
            //  if it is not blocking then you will have some unexpected behaviour
            var stream = new CliStream(); 

            var cmd = CliWrap.Cli.Wrap(ExecName)
                .WithArguments(Arguments)
                .WithStandardErrorPipe(CliWrap.PipeTarget.ToStringBuilder(stdErrBuffer))
                .WithStandardOutputPipe(CliWrap.PipeTarget.ToStringBuilder(stdOutBuffer))
                .WithStandardInputPipe(CliWrap.PipeSource.FromStream(stream))
                .WithWorkingDirectory(WorkingDirectory);
            
            cmd.ExecuteAsync();

            String? input;
            do
            {
                Thread.Sleep(10000); // Sleep to be sure that "test" app generated output

                Console.Write($"[Standard Output]{stdOutBuffer}\t[New Input]");

                stdOutBuffer.Clear();

                input = Console.ReadLine();
                
                var buffer = Encoding.UTF8.GetBytes(input + Environment.NewLine);
                stream.Write(buffer);
            }
            while (input != "quit");

Thanks to everyone for contributing.

1
Evk On

Problem is Peek is non-blocking call which does not wait for data to become available. You start new process and then immediately proceed checking its standard output with Peek, but it might be nothing there yet. This is what you observe. Instead - you should read until some stopping point, but in this case there is no such point, so you can introduce it - use Console.WriteLine instead of Console.Write here:

Console.WriteLine($"previous input ==> {line}, type next input> ");

Now on receiving end you can read until you meet newline character:

line = process.StandardOutput.ReadLine();

Note that this is blocking read. It will read until newline, and if data is not available yet - it will wait until it's there. Now "messages" in your communication have clear boundaries.

I would even say that you should forget that Peek() exist and never use it. I've never used it in my practice and all usages I ever saw lead to bugs like this.

3
radian On

If your platform is Windows, try PeekNamedPipe.

    static string ReadAvailableString(StreamReader reader)
    {
        [DllImport("kernel32.dll")]
        static extern bool PeekNamedPipe(
            SafeFileHandle hNamedPipe,
            IntPtr lpBuffer,
            int nBufferSize,
            IntPtr lpBytesRead,
            out int lpTotalBytesAvail,
            IntPtr lpBytesLeftThisMessage
            );

        var stream = (FileStream)reader.BaseStream;
        if( !PeekNamedPipe(stream.SafeFileHandle, IntPtr.Zero, 0, IntPtr.Zero, out var totalbytesAvail, IntPtr.Zero) || totalbytesAvail<=0 )
            return String.Empty;

        Span<byte> buf = stackalloc byte[totalbytesAvail];
        stream.Read(buf);
        return reader.CurrentEncoding.GetString(buf);
    }

Here is an example of getting StandardOutput.

    var output = ReadAvailableString(process.StandardOutput);