Search code examples
c#windowscmdprocesssystem.diagnostics

Creating Real-Time Input & Output Stream To Command Prompt in C#


General Question

How would one create a direct input / output stream to and from an instance of command prompt (cmd.exe) using some form of a C# application, whether it be WinForms, WPF, or UWP. Effectively using CMD as if it were just a compiled library.

What I Actually Need

I need to be able to read & write to an instance of command prompt as if I were using the application directly, meaning that a viable solution would need to be able to:

  • Programmatically write input to an instance of a CMD Process as commands (being able to pass ping google.com to CMD using C#)
  • Programmatically Read the Output from the CMD Process as it's being generated (in the case of ping google.com I should be able to pick up each individual ping as it's own line, as its being generated. In other words:
Pinging google.com [172.217.6.14] with 32bytes of data:
Reply from 172.217.6.14: bytes=32 time=31ms TTL=117
Reply from 172.217.6.14: bytes=32 time=29ms TTL=117
Reply from 172.217.6.14: bytes=32 time=46ms TTL=117
Reply from 172.217.6.14: bytes=32 time=26ms TTL=117
Ping statistics for 172.217.6.14:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 26ms, Maximum = 35ms, Average = 29ms

Each of those code blocks should be able to be referenced individually as they are generated, whether that means writing them to a textbox or outputting them to the console line-by-line.

What I Have Tried

I have already looked at these threads which provide answers similar to what I need, but not exact ones: StandardOutput.ReadToEnd() hangs [duplicate], What's the right c# input stream for cmd.exe encoding, Capturing console output from a .NET application (C#), & Reading from a process, StreamReader.Peek() not working as expected

For the sake of keeping things short, this example is for a console application (.NET Framework).

I have tried creating a simple while loop using the StreamReader.Peek() method to see if the end of the stream has been reached, although that has its own problems (namely it doesn't update dynamically, so once CMDOutput.Peek() reaches -1 it doesn't go back up, even if the stream was updated since then). An example of the code I used for this solution can be found below:

ProcessStartInfo CMDStartInfo = new ProcessStartInfo(@"C:\Windows\System32\cmd.exe", "/c ping google.com");

CMDStartInfo.UseShellExecute = false;
CMDStartInfo.ErrorDialog = false;

CMDStartInfo.RedirectStandardError = true;
CMDStartInfo.RedirectStandardInput = true;
CMDStartInfo.RedirectStandardOutput = true;

Process CMD = new Process();
CMD.StartInfo = CMDStartInfo;
bool CMDStarted = CMD.Start();

StreamWriter CMDInput = CMD.StandardInput;
StreamReader CMDOutput = CMD.StandardOutput;
StreamReader CMDErrors = CMD.StandardError;

while (true)
{
    while (CMDOutput.Peek() > -1)
    {
        try
        {
            Console.WriteLine(CMDOutput.ReadLine());

        } 
        catch
        {
            return;
        }
    }

    Console.WriteLine(CMDOutput.Peek());
    CMDInput.WriteLine(Console.ReadLine());

}

Again, you can see that the main issue with this solution is that after the first line of output(Pinging google.com [172.217.6.14] with 32bytes of data:) CMDOutput.Peek() returns -1 indefinitely, and no more output can be read.

I tried using a different approach, by removing the loop and changing the starting argument to just /K, and using StreamReader.ReadToEnd() in place of StreamReader.ReadLine() which kinda works? I can read multiple lines now, however it only runs once, and when using the ping command it simply won't output. Not to mention the application also seems to get hung up on the last line.

Side Note: The only multi-line output I tried to get with this approach was from an invalid command.

Process CMD = new Process();
CMD.StartInfo.FileName = "cmd.exe";
CMD.StartInfo.Arguments = "/K";
CMD.StartInfo.UseShellExecute = false;
CMD.StartInfo.RedirectStandardInput = true;
CMD.StartInfo.RedirectStandardOutput = true;
CMD.StartInfo.WorkingDirectory = @"C:\";
CMD.Start();

StreamWriter CMDInput = CMD.StandardInput;
StreamReader CMDOutput = CMD.StandardOutput;

string InputString = Console.ReadLine();

CMDInput.WriteLine(InputString);

string OutputString = CMDOutput.ReadToEnd();

Console.WriteLine(OutputString);

After searching and experimenting for what felt like ages, I'm just not sure where to go from here.

Solution [Lemon Sky]:

Thank you Lemon Sky for the quick and simple response. For anyone else who has a my problem the code I used to solve it can be found below. Apparently an event listener already exists for the exact thing I needed, all I had to do as Lemon Sky said is subscribe to it (Process.OutputDataReceived). Without any filtering you will still receive the line with your command, so as a simple fix I added some filtering to detect if the line being output started with C:\.

Process CMD = new Process();
CMD.StartInfo.FileName = "cmd.exe";
CMD.StartInfo.Arguments = "/K";
CMD.StartInfo.UseShellExecute = false;
CMD.StartInfo.RedirectStandardInput = true;
CMD.StartInfo.RedirectStandardOutput = true;
CMD.StartInfo.WorkingDirectory = @"C:\";
CMD.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
{

    if (!String.IsNullOrEmpty(e.Data))
    {
        if (e.Data.StartsWith(@"C:\"))
        {
            return;
        }
        else
        {
            Console.WriteLine(e.Data);
        }
    }
});
CMD.Start();

CMD.BeginOutputReadLine();

StreamWriter CMDInput = CMD.StandardInput;

while (true && !CMD.HasExited)
{
    string InputString = Console.ReadLine();
    if (InputString == "cls" || InputString == "clear")
        Console.Clear();
    else
        CMDInput.WriteLine(InputString);
}

Thanks again Lemon Sky!


Solution

  • Set process.StartInfo.RedirectStandardOutput = true, subscribe to process.OutputDataReceived, and call process.BeginOutputReadLine. This will work for text-based output delimited by newlines.