Using .NET Process for interactive control [duplicate]

20 hours ago 1
ARTICLE AD BOX

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
Read Entire Article