Introduction
This article describes a C# utility that acts as a wrapper for executing Windows commands, either as a singular command or as a list of commands to be executed asynchronously. It runs commands while capturing such result information as any error or information message captured from the Windows command. This utility also monitors the execution time and retrieves whether a command executes within a time limit or runs over it.
Background
Explanation of Need
The overall industry trend is to virtualize one’s software, not tying it to a particular computer. This provides the benefit of being able to load balance among other things. In contrast, though, there are several cases in which I actually need to run Windows commands (such as .bat or .cmd files) on a specific computer, and thus, not make use of virtualization. Additionally, I want to write C# programs which direct a workflow process, executing Windows commands while retrieving any error or information messages from the invoked running process, as well as, whether a process is running longer than anticipated.
I previously mentioned that I want to write C# programs and run Windows commands from them, and if you’re wondering why I don’t just write everything in C#, I’ll explain a couple of example scenarios below.
Firstly, I have some legacy batch files which haven’t yet met enough of a cost to benefit ratio for me to make the effort to re-write in C#. Secondly, I also have some third party applications which either don’t have an API (application programming interface), or have one but is not feature rich enough to compare with its array of Windows command line switches for execution.
As you can see, I need to run Windows commands but also want to leverage the functionality C# provides in orchestrating the overall workflow process. Additionally, I want to re-use the same C# code for performing this from multiple programs.
So, I created a C# utility that acts a wrapper for executing Windows commands, returning relevant result information back to the calling C# code from the command process. For example, I have several multi-step administrative tasks written in C# which are specific machine dependent. I use this C# Windows command running utility toward this purpose. Perhaps you have the same need, too, and I’d like to share the code of the utility I created with you.
Security Disclaimer (Swim at Your Own Risk...)
You’ve probably visited a swimming area with a sign posted such as "No Diving." In the case of the usage of the WinCmdRunner
utility, such advice would not be appropriate since we can "dive in", having a legitimate reason and safe means for using the utility under the correct circumstances. But, I do have to admit that we have to be careful concerning security when using this utility. The directive of either "No lifeguard on duty" or "Swim at your own risk" does apply here.
Realize there is a hacking vulnerability associated with allowing something to execute direct Windows commands. For my personal use, I solve this concern through individually registering specific programs to use the WinCmdRunner
, disallowing anything else from using it. For example, only members of a particular group are allowed to access the utility DLL. I’ll leave it to you how you want to address this security concern.
Using the Code
Composition of the Visual Studio Solution
The Visual Studio solution of WinCmdRunner
is comprised of two projects written using .NET Framework 4.6.1. The program is contained within a project named CoreDistributable
. I also created a Microsoft test project (named TestProject
) which contains tests that demonstrate the utilities example usage, as though the utility were called upon by another C# program.
Erecting a Facade for the Windows Command Wrapper
Notice that the CoreDistributable
project contains a publicly scoped class named CmdExecutor
and an internally scoped class named InternalInvoker
. The reason I did this was because I wanted to abstract away the details of how Windows commands are executed from calling C# programs. All the calling programs need to know is whether they want to execute a single command or a list of commands asynchronously. Only the InternalInvoker
needs to know how this is done, such as process parameters, etc.; and, the coordination of this is handled by the CmdExecutor
.
Command Validation, Can There Be Any?
Because we are running Windows commands, there is admittedly very little we can do in terms of validating a request to run a Windows command prior to actually executing it. That being said, the only thing I could think of validating is to check whether a request is empty and whether the cmd.exe can be found at the expected location. Although it might be overkill, in addition to creating a CmdValidator
class for this, I also created an interface ICmdValidator
. Therefore, I’ll leave it to you if you think of any other validation which can be performed, or multiple validations in various circumstances if you so desire.
The RequestAttributes Class
The RequestAttributes
class was created to contain the only two attributes I could think of which are pertinent to a Windows command. I choose to place the actual Windows command within the property RequestString
. In order to track whether a given Windows command runs within a specified length of time, I decided to create the property ExecutionLimitSeconds
for this purpose. Below is a display of the source code of this class.
public class RequestAttributes
{
public string RequestString { get; set; } = string.Empty;
public int ExecutionLimitSeconds
{ get; set; } = int.Parse(ConfigurationManager.AppSettings["ExecutionLimitSecondsDefault"]);
}
Windows Command Request and Result Objects
In order to facilitate easy management and recognition, I decided to create several data transformation objects which aggregate a single or multiple Windows command requests and results of executing such requests. In the main project, the CoreDistributable
project, you’ll notice that I created a folder named Dtos to contain these classes who objects are to be used for this purpose.
Objects Used for Making a Single Request
Let me begin this discussion by starting with the simplest case, a single Windows command execution request. The code displayed below shows the two properties of the SingleRequestDto
, WinCmdAbsoluteLocation
and RequestAttributes
(which I previously described in the above section). The WinCmdAbsoluteLocation
property is used to store the absolute path of where the cmd.exe is located on the particular computer where the utility is being run on.
public class SingleRequestDto
{
public string WinCmdAbsoluteLocation { get; set; } =
@ConfigurationManager.AppSettings["WinCmdAbsoluteLocation"];
public RequestAttributes RequestAttributes { get; set; }
}
In order to capture the result state of an executed Windows command, I created the below shown class named SingleResultDto
.
public class SingleResultDto
{
public enum ProcessStates
{
NotStarted,
Started,
Succeeded,
Failed
}
public int? ProcessIdDuringExecution { get; internal set; }
public ProcessStates ProcessState { get; internal set; } = ProcessStates.NotStarted;
public string OutputMsg { get; internal set; } = string.Empty;
public string ErrMsg { get; internal set; } = string.Empty;
}
Notice the ProcessStates
enumeration and property of ProcessState
which uses it. Every Windows command request begins in a state of NotStarted
. Once a Windows command begins running, its state should be assigned as Started
, and its process Id should be assigned to the property ProcessIdDuringExecution
. Likewise, once a Windows command process completes, its state should be marked as Succeeded
if it ran successfully, or Failed
if it failed. If it failed, any error message will be captured and populated into the ErrMsg
property. Additionally, any informative message will be captured in the OutMsg
property.
Example of Making a Single Windows Command Execution Request
Observe the TestCmdExecutor
class in the TestProject
of the solution. Specifically, notice the test method named Test_RunSynchronous_Good
whose code is displayed below.
In this example, I set the execution time limit to 20 seconds while issuing a request for the Windows command to wait 1 second. Obviously, when the CmdExecutor
runs this single request, it finishes successfully because the "timeout
" command is far less than what was set for the execution time limit. Observe the test method named Test_RunSynchronous_Bad
to view the opposite case, when the time limit is less than the actual execution time it ran the Windows command.
[TestMethod]
public void Test_RunSynchronous_Good()
{
var requestDto = new SingleRequestDto
{
RequestAttributes = new RequestAttributes
{
ExecutionLimitSeconds = 20,
RequestString = "timeout 1"
}
};
var resultDto = CmdExecutor.RunSingleRequest(requestDto);
Assert.IsTrue(string.IsNullOrWhiteSpace(resultDto.ErrMsg));
Assert.IsTrue(resultDto.ProcessState == SingleResultDto.ProcessStates.Succeeded);
Assert.IsTrue(resultDto.ProcessIdDuringExecution.HasValue);
}
Objects Used for Running Multiple Requests Asynchronously
Because multiple requests will have multiple results, there needs to be a means of associating a particular request with its own result. For this, the RequestResultPairDto
was created.
public class RequestResultPairDto
{
public RequestAttributes RequestAttributes { get; internal set; }
public SingleResultDto Result { get; internal set; }
}
The class MultipleRequestDto
has a property to store the absolute path of cmd.exe on a particular computer. It also has a property containing a list of RequestAttributes
, one per each requested Windows command to be executed.
public class MultipleRequestDto
{
public string WinCmdAbsoluteLocation { get; set; } =
@ConfigurationManager.AppSettings["WinCmdAbsoluteLocation"];
public List<RequestAttributes> Requests { get; set; }
}
The class MultipleResultDto
has a property with a list of RequestResultPairDto
objects. This way, each request from a list of requests can be paired with its own result so that they can be distinguished.
public class MultipleResultDto
{
public List<RequestResultPairDto> RequestResultList
{ get; internal set; } = new List<RequestResultPairDto>();
}
The method Test_RunAsynchronous
found in the TestCmdExecutor
class provides an example of running three command requests asynchronously.
The Code that Runs a Windows Command as a Process
Shown below is the primary code of the program, the InternalInvoker
’s ExecuteCmd
method.
This method performs three basic actions in order. It first performs a sanity check, making sure that it only continues to run on the condition that the request is valid to the best of its knowledge. Secondly, it performs initialization such as assigning the process parameters needed for execution of the Windows command. Thirdly, it creates a new process while using event handlers to capture any output and error messages generated during execution of the process. Note that it uses a process’ WaitForExit
property to monitor whether a process completes within the requested time period.
internal void ExecuteCmd()
{
if (!IsValidRequest) return;
InvocationRequest.RequestAttributes.RequestString = "/C " +
InvocationRequest.RequestAttributes.RequestString;
var processStartInfo = new ProcessStartInfo(fileName: InvocationRequest.WinCmdAbsoluteLocation,
arguments: InvocationRequest.RequestAttributes.RequestString)
{
Verb = "runas",
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden,
RedirectStandardOutput = true,
RedirectStandardError = true
};
try
{
using (var process = new Process { StartInfo = processStartInfo })
{
process.ErrorDataReceived += ProcessErrorDataHandler;
process.OutputDataReceived += ProcessOutputDataHandler;
InvocationResult.ProcessState = SingleResultDto.ProcessStates.Started;
process.Start();
process.BeginErrorReadLine();
InvocationResult.ProcessIdDuringExecution = process.Id;
process.BeginOutputReadLine();
if (process.WaitForExit
(InvocationRequest.RequestAttributes.ExecutionLimitSeconds * 1000))
{
InvocationResult.ProcessState =
SingleResultDto.ProcessStates.Succeeded;
}
else
{
InvocationResult.ErrMsg +=
"Process did not end within time limit" + Environment.NewLine;
}
}
}
catch (Exception ex)
{
ExecuteCmd_Handle_UnexpectedException(ex);
}
}
Points of Interest
Although the code makes use of a configuration file, the settings contained within may also be placed within a constants class instead. I merely placed such parameters into a configuration file because I didn’t want to assume that all use cases would require the same settings for all distributions of the code.
I am a web, software, and database developer having primarily a .Net and SQL Server concentration. I am also interested in leveraging other technologies where they are best utilized.