How to Get Output from a Command to Appear in a Control on a Form in Real-Time

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

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

How to display output of cmd commands in a TextBox

Well, you can adopt some existing code. (not a bad idea). However, a lot of examples have all kinds of fancy async code, event code. There is HUGE and MASSIVE reason for 50+ years, that hello world programs still exist!!!

They allow people starting out to learn.

So, I suggest you drop in a text box, a button, and then a LARGER multi-line text box.

Say like this form

Sample Image

So, we can enter a command prompt. When we hit run command, it will run that command, and capture the out put.

Code behind that button is this:

Private Sub cmdMyShell_Click(sender As Object, e As EventArgs) Handles cmdMyShell.Click

Dim MyProcess As New Process
MyProcess.StartInfo.FileName = "cmd.exe"
MyProcess.StartInfo.Arguments = " /C" & TextBox1.Text

MyProcess.StartInfo.UseShellExecute = False
MyProcess.StartInfo.RedirectStandardOutput = True
MyProcess.Start()

' get the output
Dim strMyBuffer As String = ""

strMyBuffer = MyProcess.StandardOutput.ReadToEnd
MyProcess.WaitForExit()

' now display this output in the text box:
TextBox2.Text = strMyBuffer

End Sub

Ok, so when I run above, get this:

Sample Image

Or, how about say ipconfig.

Sample Image

So, the above is a really simple bit of code. And if you make the text box large enough (and anchor to the right + bottom), then you can re-size the window.

eg this:

Sample Image

I also used a fixed font size for the text box (Courier new 12).

So above is rather bare bones. If you actually really looking to expand on this? Then I think the other links and sample code are better examples.

I am tempted to post a shell() command with the "pipe" > redirection of output to a text file. but, then that would require you to read a created text file for each commands output from a text file.

C# Unable to read output from .exe when executing

Okay, so i figured out why the output wasn't being recorded.
I was simply missing 2 lines of code...

 cmd.StandardInput.Flush();
cmd.StandardInput.Close();

AND

I had to remove runas and instead inherit the admin perms from the application itself.
By "inherit the admin perms" i mean simply to make sure the application it always being executed as administrator.

I have no idea why this exactly fixes my problem BUT it seems to work perfectly now.
If anyone knows why this works i'd love to know.

C# Process.Start: how to read output?

Firstly, you are checking for process.HasExited in a while loop.
This, of course will be false by default, and then your code will just skip this. That is why I suggest using an asynchronous method or an event-based aproach.

If you choose asynchronous, you can do this:

using (var process = Process.Start(psi))
{
errors = await process.StandardError.ReadToEndAsync();
results = await process.StandardOutput.ReadToEndAsync();
}

here, psi is an instance of ProcessStartInfo.

You set them after creating the process, but you can create an object and pass that in the constructor.

If you cannot make it asynchronous, you can do:

using (var process = Process.Start(psi))
{
errors = process.StandardError.ReadToEndAsync().Result;
results = process.StandardOutput.ReadToEndAsync().Result;
}


Related Topics



Leave a reply



Submit