Click here to Skip to main content
15,885,546 members
Articles / Programming Languages / XML

Windows Command Runner

Rate me:
Please Sign up or sign in to vote.
4.28/5 (9 votes)
21 Jun 2018CPOL8 min read 9.1K   213   16   2
This article describes a C# utility that acts as a wrapper for executing Windows commands.

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.

C#
public class RequestAttributes
{
	public string RequestString { get; set; } = string.Empty; // default

	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.

C#
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.

C#
public class SingleResultDto
    {
        public enum ProcessStates
        {
            NotStarted,
            Started,
            Succeeded,
            Failed
        }

        public int? ProcessIdDuringExecution { get; internal set; }
        public ProcessStates ProcessState { get; internal set; } = ProcessStates.NotStarted; // default
        public string OutputMsg { get; internal set; } = string.Empty; // default
        public string ErrMsg { get; internal set; } = string.Empty; // default
    }

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.

C#
[TestMethod]
public void Test_RunSynchronous_Good()
{
	// Create a request needed for the cmd executor
	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.

C#
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.

C#
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.

C#
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.

C#
internal void ExecuteCmd()
{
	// Sanity check
	if (!IsValidRequest) return;

	// Initialization
	// Prefix caller's command request to instruct that it should execute the following command
	InvocationRequest.RequestAttributes.RequestString = "/C " + 
                               InvocationRequest.RequestAttributes.RequestString;
	var processStartInfo = new ProcessStartInfo(fileName: InvocationRequest.WinCmdAbsoluteLocation,
		arguments: InvocationRequest.RequestAttributes.RequestString)
	{
		Verb = "runas",  // The process should start with elevated permissions
		UseShellExecute = false,       // Use the process rather than OS shell
		WindowStyle = ProcessWindowStyle.Hidden,
		RedirectStandardOutput = true, // Redirects the standard output 
                                               // so it reads internally in the program
		RedirectStandardError = true   // Redirects the standard error so it 
                                               // reads internally in the program
	};

	// Execute it!
	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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
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.

Comments and Discussions

 
GeneralCmd Executor Pin
Mel Pama18-Dec-18 13:57
professionalMel Pama18-Dec-18 13:57 
GeneralMy vote of 5 Pin
Member 1236439024-Jun-18 21:08
Member 1236439024-Jun-18 21:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.