How do I get output from a command to appear in a control on a Form in real-time?

A brief description of what the code performs in this example:

The shell command (cmd.exe) is run first, using start /WAIT as parameter. More or less the same functionality as /k: the console is started without any specific task, waiting to process a command when one is sent.

StandardOutput, StandardError and StandardInput are all redirected, setting RedirectStandardOutput, RedirectStandardError and RedirectStandardInput properties of the ProcessStartInfo to true.

The console Output stream, when written to, will raise the OutputDataReceived event; it’s content can be read from the e.Data member of the DataReceivedEventArgs.
StandardError will use its ErrorDataReceived event for the same purpose.
You could use a single event handler for both the events, but, after some testing, you might realize that is probably not a good idea. Having them separated avoids some weird overlapping and allows to easily tell apart errors from normal output (as a note, you can find programs that write to the error Stream instead of the output Stream).

StandardInput can be redirected assigning it to a StreamWriter stream.
Each time a string is written to the stream, the console will interpret that input as a command to be executed.

Also, the Process is instructed to rise it’s Exited event upon termination, setting its EnableRaisingEvents property to true.
The Exited event is raised when the Process is closed because an Exit command is processed or calling the .Close() method (or, eventually, the .Kill() method, which should only be used when a Process is not responding anymore, for some reason).

Since we need to pass the console Output to some UI controls (RichTextBoxes in this example) and the Process events are raised in ThreadPool Threads, we must synchronize this context with the UI’s.
This can be done using the Process SynchronizingObject property, setting it to the Parent Form or using the Control.BeginInvoke method, that will execute a delegate function on the thread where the control’s handle belongs.
Here, a MethodInvoker representing the delegate is used for this purpose.


The core function used to instantiate the Process and set its properties and event handlers:

using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;

public partial class frmCmdInOut : Form
{
    Process cmdProcess = null;
    StreamWriter stdin = null;

    public frmCmdInOut() => InitializeComponent();

    private void MainForm_Load(object sender, EventArgs e)
    {
        rtbStdIn.Multiline = false;
        rtbStdIn.SelectionIndent = 20;
    }

    private void btnStartProcess_Click(object sender, EventArgs e)
    {
        btnStartProcess.Enabled = false;
        StartCmdProcess();
        btnEndProcess.Enabled = true;
    }

    private void btnEndProcess_Click(object sender, EventArgs e)
    {
        if (stdin.BaseStream.CanWrite) {
            stdin.WriteLine("exit");
        }
        btnEndProcess.Enabled = false;
        btnStartProcess.Enabled = true;
        cmdProcess?.Close();
    }

    private void rtbStdIn_KeyPress(object sender, KeyPressEventArgs e)
    {
        if (e.KeyChar == (char)Keys.Enter) {
            if (stdin == null) {
                rtbStdErr.AppendText("Process not started" + Environment.NewLine);
                return;
            }

            e.Handled = true;
            if (stdin.BaseStream.CanWrite) {
                stdin.Write(rtbStdIn.Text + Environment.NewLine);
                stdin.WriteLine();
                // To write to a Console app, just 
                // stdin.WriteLine(rtbStdIn.Text); 
            }
            rtbStdIn.Clear();
        }
    }

    private void StartCmdProcess()
    {
        var pStartInfo = new ProcessStartInfo {
             FileName = "cmd.exe",
            // Batch File Arguments = "/C START /b /WAIT somebatch.bat",
            // Test: Arguments = "START /WAIT /K ipconfig /all",
            Arguments = "START /WAIT",
            WorkingDirectory = Environment.SystemDirectory,
            // WorkingDirectory = Application.StartupPath,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            RedirectStandardInput = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
        };

        cmdProcess = new Process {
            StartInfo = pStartInfo,
            EnableRaisingEvents = true,
            // Test without and with this
            // When SynchronizingObject is set, no need to BeginInvoke()
            //SynchronizingObject = this
        };

        cmdProcess.Start();
        cmdProcess.BeginErrorReadLine();
        cmdProcess.BeginOutputReadLine();
        stdin = cmdProcess.StandardInput;
        // stdin.AutoFlush = true;  <- already true

        cmdProcess.OutputDataReceived += (s, evt) => {
            if (evt.Data != null)
            {
                BeginInvoke(new MethodInvoker(() => {
                    rtbStdOut.AppendText(evt.Data + Environment.NewLine);
                    rtbStdOut.ScrollToCaret();
                }));
            }
        };

        cmdProcess.ErrorDataReceived += (s, evt) => {
            if (evt.Data != null) {
                BeginInvoke(new Action(() => {
                    rtbStdErr.AppendText(evt.Data + Environment.NewLine);
                    rtbStdErr.ScrollToCaret();
                }));
            }
        };

        cmdProcess.Exited += (s, evt) => {
            stdin?.Dispose();
            cmdProcess?.Dispose();
        };
    }
}

Since the StandardInput has been redirected to a StreamWriter:

stdin = cmdProcess.StandardInput;

we just write to the Stream to execute a command:

stdin.WriteLine(["Command Text"]);

The sample Form can be downloaded from PasteBin.

Console redirection in real time

Leave a Comment