Click here to Skip to main content
15,879,474 members
Articles / Programming Languages / C#

Interacting with Shell Applications (Made Easy)

Rate me:
Please Sign up or sign in to vote.
4.89/5 (17 votes)
20 Mar 2010CPOL8 min read 44.5K   2.3K   61   2
A simple class for working with Shell applications
Sample - Demo

CommandLineFun

Introduction

This article introduces a robust and effective command line abstraction class which can be used to simplify the most common tasks involved with interfacing with windows command line applications in C#. These features include the ability to launch new processes synchronously or asynchronously, the ability to capture and redirect application output, and the ability to receive call backs from command line programs, even when they do not implement any type of exit code indicator (which is required for System.Diagnostics.Process.HasExited() and WaitForExit() to determine if a process has completed execution).

Background

In recent weeks, I have been contributing artifacts for an open source project MaLa (M.A.M.E. and More Launcher), which is a front end for arcade cabinets re-configured to use a PC to store and play games instead of the typical commercial PCB (printed circuit board) used in retail arcade cabinets.

There was a need (actually a desire... by me) to create a better interface for the generation of user defined game lists. Game lists are used in the MaLa program to categorize similar arcade titles into browsable groups. Unfortunately, the MaLa project was engineered using Borland Delphi and the project leader is currently occupied with his own tasks so I needed to develop a communication path between Delphi and C# (with no experience or knowledge of Delphi). These game lists are written to disk by MaLa in a binary encoded format which can only be read by Delphi (the developer of a popular tool called ROMLister has engineered a way to write MaLa game lists in C++, but it requires an insane amount of code and math to do it!).

In any event, I created a command line Delphi program (mlgconverter.exe) and a C# graphical user interface (malaGameListEditor.exe) which provides a rich .NET user interface for creating game lists. The C# UI calls on the console/shell application mlgconverter.exe to read and write these binary files. While working on this project, I discovered several 'gotchas' in the .NET System.Diagnostics.Process classes pertaining to the use of command line applications. This article documents some of the code that sprang from this effort.

If you would like to see more on the MaLa Gamelist Editor project, you can do so HERE.

MaLa Games list Editor

MaLa Editor

What's So Hard About Creating Processes?

Working with processes is simple in .NET (even simple, but to a lesser extent in WMI), so what's the trouble? Indeed, working with processes is exceptionally simple using the Process classes.

C#
Process.Start(Path.Combine(Application.StartupPath, "mlgconverter.exe")); 

The trouble arises when you want to know when a (Shell) process has completed executing so you can continue your business logic workflow. The Process() members include a few great ways to determine if a process has completed, WaitForExit() and HasExited(). It turns out that, in Shell mode, both of these methods require the application being called to report back an exit code to signify its completion. After running several tests, it has become apparent to me that quite a lot of applications do not conform to this rule and as a result, .NET simply ignores the WaitForExit and HasExited checks and continues processing.

Additionally, there is this troublesome matter of time outs. When you overload the WaitForExit(Int32) with a timeout integer, the timeout doesn't actually apply to the process itself, merely the waiting of the caller for the process to exit. If you want the process to actually time out, then you have to kill the process in addition to simply moving along after the time out threshold is reached.

For the above reasons, and a few others, I have created a class to abstract interacting with the command line by way of a C# class called CommandLine.cs.

Using CommandLine.cs

The following section will demonstrate how to use the CommandLine.cs object to create processes which can:

  • Run either in the command line shell mode or the Windows mode
  • Run synchronously or asynchronously
  • Notify the caller when completed
  • Timeout the process and kill it
  • Display or hide the process interface from the user
  • Redirect the output of the command to anything you'd like (file, control, etc.)
Image 3

The public methods are:

  • BeginExecute() : void: Execute the process asynchronously
  • Execute() : void: Execute the process synchronously

The public properties are:

  • Error : System.Exception: The exception object if an error occurred
  • ExecutionContext : BaileySoft.Utility.ExecutionType: Enum object for Command line or Windows execution
  • Parameters : System.String: The parameter switches for the process
  • Path : System.String: The path to the program to execute
  • Runtime : System.TimeSpan: The elapsed time from when the process began to when it completed
  • ShellOutput : System.String: The stdout stream (only works in 'Windows' mode)
  • TimeOut : System.Int32?: The amount of seconds the process should run before timing out

Executing Programs at the Command Line Asynchronously

The following code segment demonstrates how to start a process at the command prompt asynchronously, in other words, the process will be started in a separate thread than the thread in which your application is running. This prevents the main thread from freezing while it waits for the process to complete. You can define whether the command prompt is displayed on screen or hidden and the amount of time the process is allowed to run before being forcibly killed.

C#
using BaileySoft.Utility;

private void MyFunction()
{
  // Default Constructor - Create empty CommandLine object
  oCommandLine = new CommandLine();
  
  // Define execution type (Windows - Visible mode is default if undefined)
  oCommandLine.ExecutionContext = ExecutionType.SHELL_EXECUTE_VISIBLE;
  
  // The program to start (required)
  oCommandLine.Path = Path.Combine(Application.StartupPath, "mlgconverter.exe");
  
  // The parameters to send to the program (optional)
  string cFile = Path.Combine(Application.StartupPath, "Fighter - Versus.mlg");  
  oCommandLine.Parameters = String.Format(" export \"{0}\"", cFile);
  
  // The amount of time (in seconds) the process can run (optional)
  oCommandLine.TimeOut = 5;
  
  // Method to execute when the process stops (optional)
  oCommandLine.HasExited += new HasExitedEventHandler(oCommandLine_HasExited);
  
  // Start the process asynchronously
  oCommandLine.BeginExecute();
}

// What to do when process exits - EventHandler
private void oCommandLine_HasExited(object sender, HasExitedEventArgs e)
{
   MessageBox.Show("Process is finally completed! It took "
	+ oCommandLine.Runtime.Seconds + " seconds to finish!");
} 

Executing Programs at the Command Line Synchronously

The following code segment demonstrates how to use CommandLine.cs to execute processes at the command prompt synchronously, in other words, the new process will run in the same thread as the application. If the application calling CommandLine.cs is a Windows Form, then it will freeze and become unusable during the period in which the called process is running.

C#
using BaileySoft.Utility;

private void MyFunction()
{
  // Default Constructor - Create empty CommandLine object
  oCommandLine = new CommandLine();
  
  // Define execution type (Windows - Visible mode is default if undefined)
  oCommandLine.ExecutionContext = ExecutionType.SHELL_EXECUTE_VISIBLE;
  
  // The program to start (required)
  oCommandLine.Path = Path.Combine(Application.StartupPath, "mlgconverter.exe");
  
  // The parameters to send to the program (optional)
  string cFile = Path.Combine(Application.StartupPath, "Fighter - Versus.mlg");  
  oCommandLine.Parameters = String.Format(" export \"{0}\"", cFile);
  
  // The amount of time (in seconds) the process can run (optional)
  oCommandLine.TimeOut = 5;
  
  // Start the process synchronously
  oCommandLine.Execute();
  
  MessageBox.Show("Process is finally completed! It took "
	+ oCommandLine.Runtime.Seconds + " seconds to finish!");
}

Executing Programs in 'Windows Mode'

You'll notice that the Enum object ExecutionType defines 4 constants:

C#
public enum ExecutionType
{
  WINDOWS_EXECUTE_VISIBLE,
  WINDOWS_EXECUTE_HIDDEN,
  SHELL_EXECUTE_HIDDEN,
  SHELL_EXECUTE_VISIBLE
} 

So what's the difference between these 4 modes? Windows Mode (as I am referring to it) is the equivalent of running a program in the Windows run menu (Start > Run). Using Windows mode allows you to fetch the output of the command you executed. If you set WINDOWS_EXECUTE_VISIBLE and that program happens to be a command line program, then the command line will be displayed but its output will be piped into the oCommandLine.ShellOutput property.

Windows Mode Vs. Shell Mode

So what mode should you use? That depends entirely on your requirements. Generally speaking, I would use SHELL_EXECUTE_VISIBLE if you want to actually display the command shell on screen with the output in the shell visible to your users.

If you don't want to display the shell onscreen and you don't care about getting the shell output, then use SHELL_EXECUTE_HIDDEN, which will run behind the scenes in hidden mode. However, If you need to get the shell output, for example to search a ping for the string 'Replied', then you will want to use WINDOWS_EXECUTE_HIDDEN.

If you select WINDOWS_EXECUTE_VISIBLE, then the called program will be displayed to the user but its output will be piped into the ShellOutput property and you should display that string in a TextBox (if you want to display the results to the user).

What about Windows Commands like Ping, Netstat, Netdom, etc.

This is a little confusing, so bear with me. If you want to run internal Windows programs like ping.exe, netstat.exe, etc., you can't define the path to the actual program (e.g., C:\Windows\System32\NETSTAT.EXE), you must define the Path as "cmd.exe" and define the command to run as a parameter:

C#
oCommandLine.Path = "cmd.exe";
oCommandLine.Parameters = "/C ping www.google.com"; 

Don't Forget the Rules of the Command Line

NOTE: Don't forget that if you use either of the SHELL modes, then you need to comply with the rules of the shell. For example, if one of your parameters is a file path and that path has spaces in it, you need to enclose it in quotation marks. As you can see from the included sample, we are calling a shell application called mlgconverter with parameters:

  • [Task] The task to execute
  • [FilePath] The path to the binary mlg file

We have escaped the FilePath parameter, so it will be sent in with quotes:

C#
// The program to start (required)
oCommandLine.Path = Path.Combine(Application.StartupPath, "mlgconverter.exe");

// The parameters to send to the program (optional)
string cFile = Path.Combine(Application.StartupPath, "Fighter - Versus.mlg");
oCommandLine.Parameters = String.Format(" export \"{0}\"", cFile);

CommandLine.cs

In the below section, I will document some of the internals of the CommandLine type.

Giving a Callback (Asynchronous Mode)

The CommandLine type exposes both synchronous and asynchronous execution. If the user is using asynchronous mode, then they need an event to subscribe to which indicates when the process has completed executing. This is accomplished by way of an event called HasExited.

C#
public class HasExitedEventArgs : EventArgs
{
  private bool _done = false;
  public HasExitedEventArgs(bool done)
  {
	this._done = done;
  }

  public bool IsCompleted
  {
    get
     { return _done; }
  }
} 

To handle the event, we use a method called <code>OnHasExited:

C#
protected virtual void OnHasExited(HasExitedEventArgs e)
{
  if (HasExited != null)
  {
    HasExited(this, e);
  }
} 

And finally the event is raised in this manner:

C#
// Raise our Exiting Event
HasExitedEventArgs e = new HasExitedEventArgs(m_isCompleted);
OnHasExited(e);

Timing out a Process (Asynchronous & Synchronous Modes)

If the user specifies a time out value, then we create a simple timer to manage this and we kill the process in the Tick event on the timer (Shell Mode).

C#
try
{
  // Sadly there is no way to accurately determine if a shell process has finished. 
  // We're going to endlessly loop until the process finishes, and throws an error. 
  // Then we know it's done. 
  Process procHandle = null;

  while (procHandle == null && !m_timeoutExpired)
  {
    if (m_TimeOutThreshold != null && pTimer == null)
    {
       CreateTimeoutClock();
    }
    else
    {
       Process.GetProcessById(m_pHandle.Id);
       Thread.Sleep(1000);
    }
  }
}
catch (ArgumentException) { }
finally
{
  m_pHandle.Close();
}

m_isCompleted = true; 

In the above code fragment, you can see that we create an endless loop that checks if the timeout value has arrived or the process has finished. To determine if the process is still running, we check its Id property (keeping in mind that WaitForExit() and HasExited() are not available to us in shellExecute mode). I'm certain there is a better way to accomplish this and I'll stand by for critical feedback :).

When the timer Tick event has fired, we kill the running process:

C#
// It's been nice, but now it's time for you to die. ;)
private void KillProcess()
{
  try
  {
    m_pHandle.Kill();
    m_pHandle.Close();
    m_pHandle.Dispose();
  }
  catch (Exception e) 
  { m_Error = e; }
}  

Other Goodies in CommandLine.cs

None of these extras were really required but since they are nice to have, I added them in.

  • Runtime: The runtime property returns a TimeSpan object which reports how long the process took to run.
  • ShellOutput: The ShellOutput returns the StdOutput that would normally be displayed on screen (Windows mode only).

History

  • 3.17.2010 - First draft

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'm a professional .NET software developer and proud military veteran. I've been in the software business for 20+ years now and if there's one lesson I have learned over the years, its that in this industry you have to be prepared to be humbled from time to time and never stop learning!

Comments and Discussions

 
GeneralGood Job !! Pin
ottovonfrankfurt12-Sep-14 0:18
ottovonfrankfurt12-Sep-14 0:18 
GeneralMy vote of 5 Pin
fychit10-Nov-10 22:05
fychit10-Nov-10 22:05 

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.