Capture command line output in C#

Capture command line output in C#

Did you ever execute a console application in C# and wanted to capture the command line output?

You’re here to find the answer.

The Scenario

You have a long running console application and want to create an UI for it. You don’t have the oppurtunity to integrate the console application as a library so you’re bound to run the executeable. You want to be able to spawn multiple processes at the same time and be able to limit the concurrent execution of the processes. You need to be able to set EnvironmentVariables for the process environment. You need to be able to respond to new lines on the StandardError and StandardInput. You might need to respond to the console application with input.

The Basics

The .Net Framework offers a handy class that allows most of the requirements the scenario holds. The class is called System.Diagnostics.Process. After I’m finished you will have a nice wrapper around the Process class that can be used in many different scenarios. How is the process class helping us? It’s pretty simple, the Process class is used to start a new process. You need to specify some information in the form of a ProcessStartInfo object and call the Start method on the Process.

Why do we need a wraper?

Since .Net Framework 4.5 we can use the TAP (Task-based Asynchronous Pattern) and that’s what I’m gonna faciliate to get a class that can be inherited for your own needs and allows you to run multiple instances of the application.

Getting Started

First we need the LimitedConcurrencyTaskScheduler I’ve provided in an earlier post. If you didn’t read that post yet go there now and atleast get the code set up in your project. While you do so leave a comment about your expirience.

Since we need a clean way to interact with the process we define an interface. This interface defines a set of properties that allow you to react to updates from the application. It also defines a set of methods that are used to generate the input for the application.

/// @license <![CDATA[Copyright © windegger.wtf 2016
///
/// Unauthorized copying of this file, via any medium is strictly
/// prohibited
///
/// Proprietary and confidential
///
/// Written by Rene Windegger <rene@windegger.wtf> on 23.02.2016]]>
/// @file Diagnostics\IEnvironmentBuilder.cs
/// Declares the IEnvironmentBuilder interface.
namespace wtf.windegger.SharedLibrary.Diagnostics
{
using System.Collections.Generic;
/// Interface for environment builder.
/// @author Rene Windegger
/// @date 23.02.2016
public interface IEnvironmentBuilder
{
/// Gets or sets a value indicating whether this object is running.
/// @return true if this object is running, false if not.
bool IsRunning { get; set; }
/// Gets or sets a value indicating whether this object is finished.
/// @return true if this object is finished, false if not.
bool IsFinished { get; set; }
/// Gets or sets the console.
/// @return The console.
string Console { get; set; }
/// Builds argument line.
/// @return A string.
string BuildArgumentLine();
/// Builds environment variables.
/// @return A Dictionary<string,string>
Dictionary<string, string> BuildEnvironmentVariables();
/// Gets the pathname of the working directory.
/// @return The pathname of the working directory.
string WorkingDirectory { get; }
}
}

Now that we have sepcified our Environment Builder Interface we are ready to implement our BaseProcess class.

The BaseProcess Class

The BaseProcess class specifies some static properties and a set of static methods. Since it is an abstract generic class those static properties and methods will be initialized per implementation. The static members are used for parallelization, we define our LimitedConcurrencyTaskScheduler used for scheduling our child processes and a LinkedList that will hold our processes. The static Start methods provide a way to enqueue a new process to the queue. The argument for that methods will be our environment builders. Our class defines two properties, the command line of the process to run and the environment that will be used during execution. The two fields are used to generate the console output, one for locking the StringBuilder and one to generate the output. The constructor of the class sets the command line so make sure to specify it in your implementation class. The protected Execute method does the actual work of spawning the new child process. The private methods are handlers for some of the events triggered by the System.Diagnostics.Process class. Those memebers will call the abstract members defined in the class.

/// @license <![CDATA[Copyright © windegger.wtf 2016
///
/// Unauthorized copying of this file, via any medium is strictly
/// prohibited
///
/// Proprietary and confidential
///
/// Written by Rene Windegger <rene@windegger.wtf> on 24.02.2016]]>
/// @file Diagnostics\BaseProcess.cs
/// Implements the base process class.
namespace wtf.windegger.SharedLibrary.Diagnostics
{
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using Threading;
using System.Diagnostics;
/// See .
/// @author Rene Windegger
/// @date 24.02.2016
/// @sa System.Diagnostics.Process
public abstract class BaseProcess<T, TEnvironment> : Process
where T : BaseProcess<T, TEnvironment>, new()
where TEnvironment : IEnvironmentBuilder
{
#region Static Properties
/// Gets the process scheduler.
/// @return The process scheduler.
public static LimitedConcurrencyTaskScheduler ProcessScheduler { get; } = new LimitedConcurrencyTaskScheduler(1);
/// Gets the processes.
/// @return The processes.
public static LinkedList Processes { get; } = new LinkedList();
#endregion
#region Static Methods
/// Starts the given environment.
/// @author Rene Windegger
/// @date 24.02.2016
/// @param environment The environment.
/// @return A Task
public static Task Start(TEnvironment environment)
{
return Task.Factory.StartNew(() => new T())
.ContinueWith(p => { Processes.AddLast(p.Result); return p.Result; })
.ContinueWith(p => { p.Result.Execute(environment); return p.Result; }, ProcessScheduler)
.ContinueWith(p => { Processes.Remove(p.Result); return p.Result; });
}
/// Starts.
/// @author Rene Windegger
/// @date 24.02.2016
/// @param environment The environment.
/// @param token The token.
/// @return A Task
public static Task Start(TEnvironment environment, CancellationToken token)
{
return Task.Factory.StartNew(() => new T(), token)
.ContinueWith(p => { Processes.AddLast(p.Result); return p.Result; }, token)
.ContinueWith(p => { p.Result.Execute(environment); return p.Result; }, token, TaskContinuationOptions.LongRunning, ProcessScheduler)
.ContinueWith(p => { Processes.Remove(p.Result); return p.Result; }, token);
}
#endregion
#region Properties
/// Gets the command line.
/// @return The command line.
public string CommandLine { get; }
/// Gets the environment.
/// @return The environment.
public TEnvironment Environment { get; private set; }
#endregion
#region Fields
private StringBuilder m_ConsoleBuilder = new StringBuilder(); ///< The console builder
private readonly object m_DataReceivedLock = new object(); ///< The data received lock
#endregion
/// Initializes a new instance of the
/// wtf.windegger.SharedLibrary.Diagnostics.BaseProcess<T,
/// TEnvironment> class.
/// @author Rene Windegger
/// @date 24.02.2016
/// @param commandLine The command line.
public BaseProcess(string commandLine) : base()
{
CommandLine = commandLine;
}
#region Protected Methods
/// Executes the given environment.
/// @author Rene Windegger
/// @date 24.02.2016
/// @param env The environment.
protected void Execute(TEnvironment env)
{
Environment = env;
ProcessStartInfo info = new ProcessStartInfo(CommandLine, Environment.BuildArgumentLine());
info.RedirectStandardError = true;
info.RedirectStandardInput = true;
info.RedirectStandardOutput = true;
info.UseShellExecute = false;
info.CreateNoWindow = true;
info.WorkingDirectory = Environment.WorkingDirectory;
foreach (var e in Environment.BuildEnvironmentVariables())
{
#if NET451
info.EnvironmentVariables.Add(e.Key, e.Value);
#else
info.Environment.Add(e.Key, e.Value);
#endif
}
StartInfo = info;
ErrorDataReceived += DataReceived;
OutputDataReceived += DataReceived;
Start();
Environment.IsRunning = true;
BeginErrorReadLine();
BeginOutputReadLine();
#if NET451
AppDomain.CurrentDomain.ProcessExit += ApplicationExit;
#endif
WaitForExit();
if (HasExited)
{
CancelErrorRead();
CancelOutputRead();
#if NET451
Close();
AppDomain.CurrentDomain.ProcessExit -= ApplicationExit;
#endif
Environment.IsRunning = false;
Environment.IsFinished = true;
}
}
#endregion
#region Private Methods
/// Application exit.
/// @author Rene Windegger
/// @date 24.02.2016
/// @param sender Source of the event.
/// @param e Event information.
private void ApplicationExit(object sender, EventArgs e)
{
ShutdownRequested();
}
/// Data received.
/// @author Rene Windegger
/// @date 24.02.2016
/// @param sender Source of the event.
/// @param e Data received event information.
private void DataReceived(object sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.Data?.Trim()))
{
string line = e.Data.Trim();
lock (m_DataReceivedLock)
{
m_ConsoleBuilder.AppendLine(line);
}
Environment.Console = m_ConsoleBuilder.ToString();
LineReceived(line);
}
}
#endregion
#region Abstract Methods
/// Shutdown requested.
/// @author Rene Windegger
/// @date 24.02.2016
protected abstract void ShutdownRequested();
/// Line received.
/// @author Rene Windegger
/// @date 24.02.2016
/// @param line The line.
protected abstract void LineReceived(string line);
#endregion
}
}

So Now that we have the Base Process class ready we can start to make use of it in one of our projects.

This post is also available in: German