3

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.

Jimi
  • 29,621
  • 8
  • 43
  • 61
Cyberclops
  • 311
  • 2
  • 17
  • when you set the breakpoint on the txt.Text += line; are you running the code or stepping through? If the first, maybe it is invoking the method a second time before it has actually started the first time and causing an issue there. Just something to try. – Troy Mac1ure Aug 03 '18 at 21:54
  • I'm pretty sure I tried both ways ... Continue and Step. – Cyberclops Aug 03 '18 at 22:05
  • Try putting the whole Exec.OutputHandler code in it's own function and see if that helps. Change the line: { txt.Text += line; })); to call the function you created instead. – Troy Mac1ure Aug 03 '18 at 22:16
  • Do you mean [something like this](https://i.imgur.com/k3lQj8t.gifv)? You can use the Process events (including the `Exited` event, enabled setting `EnableRaisingEvents = true;`), ditch `WaitForExit` and redirect the `StandardInput` to a `StreamWriter` when you can write your commands. If you need a sample code, let me know. – Jimi Aug 04 '18 at 00:23
  • @Jimi ... I could definitely use the sample code please. – Cyberclops Aug 04 '18 at 03:17

1 Answers1

12

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"]);

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
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Excellent! I will have to try this out and see if I can get it to work for me. – Cyberclops Aug 05 '18 at 00:10
  • @Cyberclops If you need a hand to implement this code in some specific context, just ask. – Jimi Aug 05 '18 at 03:47
  • I am trying to put together a class that I can reuse for a variety of cases like this. I want to have the ability to run with no output and just get the return code, the ability to run and get the return code and output returned to the caller, and the ability to pass in a handler that can process the output in real-time. – Cyberclops Aug 05 '18 at 04:05
  • For the first case, you already have a practical solution. The second depends on the external process. With `cmd.exe`, you have what you need. Should you have to interact with processes that have their own *environment* (`netsh`, `ftp`, `telnet`etc.), you probably want to execute them directly. I'm not sure what you are referring to in the last part. Anyway, this is theory. If you have some specific coding issue, post another specific question and maybe let me know (posting it's link here). I have a number of useless notions on this matter :) – Jimi Aug 05 '18 at 05:01
  • For the last part what I mean is I want code that handles the output from stdout/stderr to be able to call a handler supplied by the user to process the output. The code example you provided is not generic enough for my final purposes. The code uses hard coded items for the object to receive the output, rtbStdOut and rtbStdErr, and hard codes the setting for SyncronizingObject as this. For a generic solution these would have to be something provided by the caller. I tried recoding your example code this way but I was unable to get it to work properly. – Cyberclops Aug 07 '18 at 15:24
  • You just need to extend the method call to include the Objects references you want to use: `private void StartCmdProcess(Control StdOut, Control StdErr, Stream StdIn)`. Use those references to update the UI. But you could also just use the Streams (passing them as the method parameters) and let the user update its UI reading the streams. Write `e.Data` on each of the streams reference object. – Jimi Aug 07 '18 at 15:31
  • Well done, I'm sure you both learned a lot that can be applied in the future. However, this does seem to be a case of reinventing the wheel. Why not just use [Windows Terminal](https://github.com/microsoft/terminal)? – Jodrell Jul 18 '22 at 16:15