1

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]
  • The following may be helpful: https://stackoverflow.com/a/72818271/10024425 – Tu deschizi eu inchid Dec 19 '22 at 14:21
  • I cann not use "EnableRaisingEvents = true" because "test" app do not use Console.WriteLine but Console.Write and I cannot change that. So, after starting process and calling "process.BeginOutputReadLine()" no events are generated and I do not get even the first line that I can get using "StandardOutput..Peek()" and "StandardOutput..Read()" – Jerko Sladoljev Dec 19 '22 at 21:17
  • The following may be helpful: https://stackoverflow.com/a/472210/10024425 and https://learn.microsoft.com/en-us/dotnet/api/system.console?redirectedfrom=MSDN&view=net-7.0#Buffer – Tu deschizi eu inchid Dec 20 '22 at 04:20
  • Please add for a console application that replicates the console application that you're attempting to execute. – Tu deschizi eu inchid Dec 20 '22 at 04:22
  • the real console app that I try to execute is proprietary and it acts similar like "cmd" on windows. It query for input command and executes it on solution arhitecture affecting data and arhitecture elements. I am trying to automate it so C# could automatically execute set of desired commands to speed up process. All is running under linux docker container. So, you can actually use "cmd" or "bash" (for linux) as "test" app. – Jerko Sladoljev Dec 20 '22 at 12:50

3 Answers3

1

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);
radian
  • 199
  • 5
0

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.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • Thank you for response. Please understand that "test" app is a simulation of real app which generates similar outputs and which is not under my control, so I cannot change it. Any solution which includes changing of "test" app is not possible. I added "Thread.Sleep(10000) before calling "process.StandardOutput.Peek()" so I can be sure that "test" app generated output but still I have the same problem. What I am observing is that "process.StandardOutput.Peek()" does not work after call to "process.StandardInput.WriteLine(input);". Do you know why Peek() works only in first loop pass? – Jerko Sladoljev Dec 19 '22 at 20:57
0

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.