Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

From various sources on the web, I have put together the following code for executing a command via CMD.exe and capturing output from STDOUT and STDERR.

public static class Exec
{
    public delegate void OutputHandler(String line);

    // <summary>
    /// Run a command in a subprocess
    /// </summary>
    /// <param name="path">Directory from which to execute the command</param>
    /// <param name="cmd">Command to execute</param>
    /// <param name="args">Arguments for command</param>
    /// <param name="hndlr">Command output handler (null if none)</param>
    /// <param name="noshow">True if no windows is to be shown</param>
    /// <returns>Exit code from executed command</returns>
    public static int Run(String path, String cmd, String args,
                          OutputHandler hndlr = null, Boolean noshow = true)
    {
        // Assume an error
        int ret = 1;
        // Create a process
        using (var p = new Process())
        {
            // Run command using CMD.EXE
            // (this way we can pipe STDERR to STDOUT so they can get handled together)
            p.StartInfo.FileName = "cmd.exe";
            // Set working directory (if supplied)
            if (!String.IsNullOrWhiteSpace(path)) p.StartInfo.WorkingDirectory = path;
            // Indicate command and arguments
            p.StartInfo.Arguments = "/c \"" + cmd + " " + args + "\" 2>&1";
            // Handle noshow argument
            p.StartInfo.CreateNoWindow = noshow;
            p.StartInfo.UseShellExecute = false;
            // See if handler provided
            if (hndlr != null)
            {
                // Redirect STDOUT and STDERR
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.RedirectStandardError = true;
                // Use custom event handler to capture output
                using (var outputWaitHandle = new AutoResetEvent(false))
                {
                    p.OutputDataReceived += (sender, e) =>
                    {
                        // See if there is any data
                        if (e.Data == null)
                        {
                            // Signal output processing complete
                            outputWaitHandle.Set();
                        }
                        else
                        {
                            // Pass string to string handler
                            hndlr(e.Data);
                        }
                    };
                    // Start process
                    p.Start();
                    // Begin async read
                    p.BeginOutputReadLine();
                    // Wait for process to terminate
                    p.WaitForExit();
                    // Wait on output processing complete signal
                    outputWaitHandle.WaitOne();
                }
            }
            else
            {
                // Start process
                p.Start();
                // Wait for process to terminate
                p.WaitForExit();
            }
            // Get exit code
            ret = p.ExitCode;
        }
        // Return result
        return ret;
    }

    // <summary>
    /// Run a command in a subprocess and return output in a variable
    /// </summary>
    /// <param name="path">Directory from which to execute the command</param>
    /// <param name="cmd">Command to execute</param>
    /// <param name="args">Arguments for command</param>
    /// <param name="outp">Variable to contain the output</param>
    /// <returns>Exit code from executed command</returns>
    public static GetOutputReturn GetOutput(String path, String cmd, String args)
    {
        GetOutputReturn ret = new GetOutputReturn();
        ret.ReturnCode = Run(path, cmd, args, (line) =>
                             {
                               ret.Output.AppendLine(line);
                             });
        return ret;
    }
}

public class GetOutputReturn
{
    public StringBuilder Output = new StringBuilder();
    public int ReturnCode = 1;
}

I am able to use this in a console app in three different manners as follows:

static void Main(string[] args)
{
    int ret;
    Console.WriteLine("Executing dir with no capture and no window");
    ret = Exec.Run(@"C:\", "dir", "");
    Console.WriteLine("Execute returned " + ret);
    Console.WriteLine("Press enter to continue ...");
    Console.ReadLine();
    Console.WriteLine("Executing dir with no capture and window");
    ret = Exec.Run(@"C:\", "dir", "", null, false);
    Console.WriteLine("Execute returned " + ret);
    Console.WriteLine("Press enter to continue ...");
    Console.ReadLine();
    Console.WriteLine("Executing dir with capture and no window");
    var results = Exec.GetOutput(@"C:\", "dir", "");
    Console.WriteLine(results.Output.ToString());
    Console.WriteLine("Execute returned " + results.ReturnCode);
    Console.ReadLine();
    Console.WriteLine("Executing dir with real-time capture and no window");
    ret = Exec.Run(@"C:\", "dir", "", ShowString);
    Console.WriteLine("Execute returned " + ret);
}

public delegate void StringData(String str);

static void ShowString(String str)
{
    Console.WriteLine(str);
}

public delegate void StringData(String str);

static void ShowString(String str)
{
    Console.WriteLine(str);
}

The first run does not gather any output and just shows the exit code.
The second run does not gather any output but shows the window.
The effect of this that the output appears in the console window real-time.
The third run uses GetOutput to gather the output.
The effect of this is that the output does not appear until the run is completed.
The last run uses a handler to receive and display the output real-time.
In appearance this looks like the second run but it is very different.
For each line of output that is received ShowString is called.
Show string simply displays the string.
However, it could do anything it needs with the data.

I am trying to adapt the last run such that I can update a text box with the output of the command in real time. The issue that I am having is how to get it in the right context (for lack of a better term). Because OutputHandler is called asynchronously, it has to use the InvokeRequired/BeginInvoke/EndInvoke mechanism to get in sync with the UI thread. I am having a little problem with how to do this with parameters. In my code the textBox could be one of several in a tab control as several background "Run"'s could be taking place.

So far I have this:

private void btnExecute_Click(object sender, EventArgs e)
{
    // Get currently selected tab page
    var page = tcExecControl.SelectedTab;
    // Get text box (always 3rd control on the page)
    var txt = (TextBox)page.Controls[2];
    // Create string handler
    var prc = new Exec.OutputHandler((String line) =>
                  {
                      if (txt.InvokeRequired)
                          txt.Invoke(new MethodInvoker(() =>
                                     { txt.Text += line; }));
                          else txt.Text += line;
                   });
    // Command and arguments are always 1st and 2nd controls on the page
    var result = Exec.Run(@"C:\", page.Controls[0].Text, page.Controls[1], prc);                              
}

But this does not seem to be working. I am not seeing any output to the txtBox.
In fact the program basically hangs in the handler.

If I change the code to use GetOutput and then write the resulting output to the text box everything works. So I know that I have the command set up properly. Using the debugger, I am able to set a break point on the "if (txt.InvokeRequired)" line and I see the first line of output coming correctly. At this point the code takes the true path of the if statement, but if I set a breakpoint on the txt.Text += line; line it never gets there.

Can anyone help me out? I'm sure I'm missing something.

like image 622
Cyberclops Avatar asked Dec 17 '25 11:12

Cyberclops


1 Answers

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; its 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 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();

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(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"]);

Console redirection in real time

The sample Form can be downloaded from PasteBin.


VB.Net version

Controls' names:
rtbStdOut -> RichTextBox (blue background), receives StdOut
rtbStdErr -> RichTextBox (in the middle), receives StdErr
rtbStdIn -> RichTextBox (at the bottom), writes to StdIn
btnStartProcess -> Button (on the right), starts the Process
btnEndProcess -> Button (on the left), stops te Process

Download this Form from Google Drive

Imports System.Diagnostics
Imports System.IO

Public Class frmCmdInOut

    Private cmdProcess As Process = Nothing
    Private stdin As StreamWriter = Nothing

    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
        rtbStdIn.Multiline = False
        rtbStdIn.SelectionIndent = 20
    End Sub

    Private Sub btnStartProcess_Click(sender As Object, e As EventArgs) Handles btnStartProcess.Click
        btnStartProcess.Enabled = False
        StartCmdProcess(Me)
        btnEndProcess.Enabled = True

    End Sub

    Private Sub btnEndProcess_Click(sender As Object, e As EventArgs) Handles btnEndProcess.Click
        If stdin.BaseStream IsNot Nothing AndAlso stdin.BaseStream.CanWrite Then stdin.WriteLine("exit")
        btnEndProcess.Enabled = False
        btnStartProcess.Enabled = True
        cmdProcess?.Close()
    End Sub

    Private Sub rtbStdIn_KeyPress(sender As Object, e As KeyPressEventArgs) Handles rtbStdIn.KeyPress
        If e.KeyChar = ChrW(Keys.Enter) Then
            If stdin Is Nothing Then
                rtbStdErr.AppendText("Process not started" + Environment.NewLine)
                Return
            End If

            e.Handled = True
            If stdin.BaseStream.CanWrite Then
                stdin.Write(rtbStdIn.Text + Environment.NewLine)
                stdin.WriteLine() ' To write to a Console app, just stdin.WriteLine(rtbStdIn.Text); 
            End If
            rtbStdIn.Clear()
        End If
    End Sub

    Private Sub StartCmdProcess(synchObj As Control)

        ' Arguments = $"start /WAIT cscript.exe script.vbs /xpr",
        ' Batch File Arguments = "/C START /b /WAIT batchfile.bat",
        ' Test: Arguments = "START /WAIT /K ipconfig /all",

        ' start with /U
        ' StandardErrorEncoding = Encoding.Unicode,
        ' StandardOutputEncoding = Encoding.Unicode,

        Dim pStartInfo = New ProcessStartInfo() With {
            .FileName = "cmd.exe",
            .Arguments = "START /WAIT",
            .CreateNoWindow = True,
            .RedirectStandardError = True,
            .RedirectStandardInput = True,
            .RedirectStandardOutput = True,
            .UseShellExecute = False,
            .WindowStyle = ProcessWindowStyle.Hidden,
            .WorkingDirectory = Application.StartupPath
        }

        cmdProcess = New Process() With {
            .EnableRaisingEvents = True,
            .StartInfo = pStartInfo,
            .SynchronizingObject = synchObj
        }

        cmdProcess.Start()
        cmdProcess.BeginErrorReadLine()
        cmdProcess.BeginOutputReadLine()
        stdin = cmdProcess.StandardInput

        AddHandler cmdProcess.OutputDataReceived,
            Sub(s, evt)
                If evt.Data IsNot Nothing Then
                    rtbStdOut.AppendText(evt.Data + Environment.NewLine)
                    rtbStdOut.ScrollToCaret()
                End If
            End Sub
        AddHandler cmdProcess.ErrorDataReceived,
            Sub(s, evt)
                If evt.Data IsNot Nothing Then
                    rtbStdErr.AppendText(evt.Data + Environment.NewLine)
                    rtbStdErr.ScrollToCaret()
                End If
            End Sub

        AddHandler cmdProcess.Exited,
            Sub(s, evt)
                stdin?.Dispose()
                cmdProcess?.Dispose()
            End Sub
    End Sub
End Class
like image 200
Jimi Avatar answered Dec 19 '25 07:12

Jimi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!