devxlogo

Managing Processes in .NET

Managing Processes in .NET

ave you ever wanted to manage processes in Win32 code? When it comes to this, there’s good and bad news. On the good news side, you can do virtually everything you desire, including block your current process waiting for another process to terminate. On the bad news side, you must work with an API that is not one for the faint-hearted. CreateProcess is the one-size-fits-all function that creates a new Win32 process and manages impersonation and security settings. This function has quite a quirky syntax. The Win32 SDK supplies several functions including CreateProcess and OpenProcess that provide the core functionalities. The Win32 SDK provides ToolHelp API, a secondary SDK, but it is not available on all possible combinations of Win32 operating systems. ToolHelp API offers more advanced functions including the capability to list modules and dependencies, capture the memory footprint, and ping process.

Fortunately, the .NET Process class allows you to gain full control over system processes. You can start and stop processes and retrieve information about running processes such as the list of loaded modules and the characteristics of the memory occupied. The class also features handy methods to know whether a process is responding or has just exited and with which return code. Programmers also have full control over the style of the window the process runs in. After an overview of the capabilities of the Process class, this article demonstrates how to hide running console processes, monitor their execution, and capture any output. I’ll use this strategy to create a sample Compression class to use with WinZip and gzip (popular tools for compressing data).

The Process class provides access to both local and remote processes and enables you to start and stop processes on the local machine. The class represents a superset of the capabilities of the CreateProcess Win32 API.

Nicely enough, .NET groups all these platform services under a single class?the Process class. Whatever you need to do that involves spawning a new process or controlling a running one, you can do from this unique (and rather powerful) entry point.

The Process Class
The Process class provides access to local and remote processes and enables you to start and stop processes on the local machine. The class represents a superset of the capabilities of the CreateProcess Win32 API. In addition, the Process class makes working with running instances of applications particularly easy. For example, you only need a single line of code to start a new process from within a .NET application:

   Process.Start("Notepad.exe")

If you need to open a particular document, say, a Word document, it’s even easier.

   Process.Start("invoice.doc")

In this case, the Process class is smart enough to figure out that the argument is not an executable. It looks up the system registry for a match on the extension (.doc in this case), gets the executable associated with that type of file, and starts it up. This behavior reveals another powerful Win32 API function working under the hood?ShellExecuteEx, which is designed to open a document?be it an executable (that is, an .exe or .bat file), or a document file associated with a program (such as a .txt or a .doc file).

The Process class has both instance and shared (static, if you speak C#) methods. Table 1 lists all the methods exposed by the class. Looking at the table will give you a clear idea of the class’ capabilities.


Table 1: Methods exposed by the Process class.

Method

Description

Close

Closes the process and frees all the resources associated with the instance.

CloseMainWindow

Sends a WM_CLOSE message to the main window of the process. Makes sense only if the process has a user interface.

EnterDebugMode

Shared method, enables the SeDebugPrivilege privilege on the current thread, meaning that the current process can operate as a debugger.

GetCurrentProcess

Shared method, returns a new instance of the Process class associated with the currently active process.

GetProcessById

Shared method, returns a new instance of the Process class associated with the process matching the specified ID and machine name.

GetProcesses

Shared method, returns an array of Process objects each associated with a currently active process.

GetProcessesByName

Shared method, returns an array of Process objects each associated with a currently active process with the specified name (for example, all instances of Notepad).

Kill

Immediately terminates the associated process.

LeaveDebugMode

Shared method, revokes the privilege that enables the current process to operate as a debugger.

Refresh

Discards any cached information about the process and resets the associated Process object.

Start

Shared method, starts a process and returns the associated Process object. This method also has an instance-based version.

WaitForExit

Stops the current process until the associated process exits or for the specified duration.

WaitForInputIdle

Causes the current process to wait for the associated process to enter an idle state.

Terminating a Process
You have two ways to terminate a process?Kill and CloseMainWindow. The former stops execution immediately, whereas the latter simply posts a termination message to the message queue. The Kill method can cause loss of data if it reaches the process at a critical stage. You must use the Kill method if you have to programmatically terminate a program that doesn’t have a graphical user interface (such as a console program or a system service). You should always use CloseMainWindow to stop GUI programs.

You may have a programming situation in which you need to spawn an external program and wait for it to terminate. By default, a spawned process runs independently from the parent. To synchronize the two so that the parent resumes when the child has completed, you use the WaitForExit method. WaitForExit is an instance-based method that requires a fresh instance of the Process class to work.

   Dim p As New Process   p = Process.Start("notepad.exe")   p.WaitForExit()

The Start method has several overloads; some shared and one instance-based. The instance-based overload doesn’t accept parameters. To specify the name of the executable to run, you need to instantiate and fill a class named ProcessStartInfo. Let’s discover more about this class and process information in general.

Discover Process Information
A running process has a lot of information to show. In first place, a running process has a unique ID that identifies it throughout its whole lifetime. If the process is a GUI program, it also has a window handle and a title string. On a more technical side, the Process class maintains information about the threads managed by the process and the modules (that is, assemblies and DLLs) loaded. Process class tracks the memory footprint and returns detailed information through a variety of properties. Table 2 lists all the properties available on the Process class.

Table 2: Properties of the Process class.

Property

Description

BasePriority

Gets the base priority of the associated process.

EnableRaisingEvents

Indicates whether the Exited event is raised upon termination.

ExitCode

Returns the exit code of the process.

ExitTime

Returns the time when the process exited.

Handle

Returns the Win32 native handle of the process.

HandleCount

Gets the number of handles opened by the process.

HasExited

Indicates whether the process has terminated.

Id

Returns the unique identifier for the associated process.

MachineName

Returns the name of the computer the process is running on.

MainModule

Returns the main module for the process.

MainWindowHandle

Returns the window handle of the main window of the process.

MainWindowTitle

Returns the caption of the main window of the process.

MaxWorkingSet

Gets or sets the maximum working set size for the process.

MinWorkingSet

Gets or sets the minimum working set size for the process.

Modules

Gets the modules that have been loaded by the associated process.

NonpagedSystemMemorySize

Gets the non-paged system memory size allocated to the process.

PagedMemorySize

Gets the paged memory size.

PagedSystemMemorySize

Gets the paged system memory size.

PeakPagedMemorySize

Gets the peak paged memory size.

PeakVirtualMemorySize

Gets the peak virtual memory size.

PeakWorkingSet

Gets the peak working set size.

PriorityBoostEnabled

Indicates whether the OS must boost the process when the main window has the focus.

PriorityClass

Gets or sets the overall priority category for the process.

PrivateMemorySize

Gets the private memory size.

PrivilegedProcessorTime

Gets the privileged processor time for this process.

ProcessName

Gets the name of the executable.

ProcessorAffinity

Indicates the processors on which the threads can be scheduled.

Responding

Indicates whether the user interface of the process is responding.

StandardError

Returns the stream used to read the application’s error output.

StandardInput

Returns the stream used to write the application’s input.

StandardOutput

Returns the stream used to read the application’s output.

StartInfo

Gets or sets the properties to pass to the Start method.

StartTime

Returns the time that the process was started.

SynchronizingObject

Gets or sets the object used to marshal the event handler calls that are issued as a result of a process exit event.

Threads

Gets the set of threads that are running in the process.

TotalProcessorTime

Gets the total processor time for this process.

UserProcessorTime

Gets the user processor time for this process.

VirtualMemorySize

Gets the size of the process’s virtual memory.

WorkingSet

Gets the associated process’s physical memory usage.

The Responding property returns a Boolean value that indicates whether the process is active and working. The value results from a ping operated on the process’ main window. Basically, when you attempt to read the value of the property, the property’s get accessor sends a Windows message to the window with a timeout of five seconds. It utilizes the SendMessageTimeout Win32 API. If the function times out then the process is considered not responding.

Since Windows Forms applications support only the single-thread apartment (STA) model, you must ensure that the thread handling the Exited event, and the thread that created the Windows Forms control you want to update, coincide.

The Process class also fires one event, Exited, when the associated process ends. This event does not automatically fire each and every time a process terminates. If you want to fire this event, turn on the EnableRaisingEvents Boolean property. This property is set to False by default.

If you decide to handle the Exited event, then you probably need to access a Windows Forms control from within the event handler to refresh the user interface. Since Windows Forms applications support only the single-thread apartment (STA) model, you must ensure that the thread handling the Exited event, and the thread that created the Windows Forms control you want to update, coincide. This is not necessarily true by default and can lead to anomalies or even exceptions. To work around this issue, you can set the SynchronizingObject property of the Process class to the Windows Forms control you’re mainly interested in (or any control on the same UI form). This guarantees that the application executes the Exited event handler on the same thread managing the form.

Displaying Process Information
To make sense of the methods and properties in Table 1 and Table 2, let’s build a sample applet that displays some of the information you normally get through the Windows Task Manager. Listing 1 shows the code that grabs some information about all the currently active processes and populates a ListView control with names, working set, and start time.

The GetProcesses shared method returns an array of Process objects, one per each active process. The code in Listing 1 walks its way through the array and adds an item to a ListView control with the values of the ProcessName, WorkingSet, and StartTime properties. This information, and in particular the working set, corresponds to that displayed by the Windows Task Manager (see Figure 1).

?
Figure 1: A sample application based on the .NET process API that mimics the Windows Task Manager.

In Figure 1 you also see a second ListView control filled with modules information. An active process consists of various modules?that is, dynamic load libraries that it depends on for some functionality. The Modules property returns a collection of ProcessModule objects, each pointing to a different library or assembly. Listing 2 shows the source code that populates the second ListView control when a process is selected.

Specify Startup Information
When you start a new process, especially if the program is a console program, you need to pass some command line arguments. How do you do that? The Start shared method has an overload that allows you to pass a command line as a string.

   Process.Start("notepad.exe", "myfile.txt")

When your application uses a shared overload of the Start method, it always creates a new Process object. If you want to start a new process based on an existing instance of the Process class, you use the instance-based version of the Start method. In this case, though, you can’t specify any parameter because the signature of the method doesn’t allow it. Any startup information should be specified through the ProcessStartInfo class.

   Dim info As New ProcessStartInfo   info.Arguments = args   info.FileName = exe   Dim p As New Process    p.StartInfo = info   p.Start()

The FileName property lets you indicate the name of the executable to spawn. The Arguments property allows you to set the command line string. However, using the ProcessStartInfo class is a bit overkill if you only need to specify the executable and a command line. There are other reasons to use startup information. Table 3 lists the properties of the ProcessStartInfo class.

Table 3: Properties of the ProcessStartInfo class.

Property

Description

Arguments

Gets or sets the command line to use when starting the application.

CreateNoWindow

Indicates whether to start the process in a new window.

EnvironmentVariables

Returns the collection of all environment variables.

ErrorDialog

Indicates whether an error dialog is displayed to the user if the process cannot be started.

ErrorDialogParentHandle

Gets or sets the window handle to use when an error dialog is shown.

FileName

Indicates the program, or the document, to start.

RedirectStandardError

Indicates whether the error output of the program must be redirected to the stream set in the StandardError member of the Process object.

RedirectStandardInput

Indicates whether the process command input is read from the stream set in the StandardInput member of the Process object.

RedirectStandardOutput

Indicates whether the process output is written to the stream set in the StandardOutput member of the Process object.

UseShellExecute

Indicates whether the program, or document, must be started through the shell using the ShellExecuteEx Win32 function.

Verb

Gets or sets the verb to use when opening the program or document.

Verbs

Returns the list of the verbs associated with the type of file specified by the FileName property.

WindowStyle

Gets or sets the window state to use when the process is started.

WorkingDirectory

Gets or sets the initial directory for the process.

In the case of redirected output, the Process class accumulates the output into a memory stream instead of just sending it out to the console output. You can then access any byte output through the Process class’ StandardOutput property.

The ProcessStartInfo class has a lot of interesting properties. I’ll focus on a few of them: CreateNoWindow, Verb, and the three redirecting properties (RedirectStandardError, RedirectStandardInput, and RedirectStandardOutput). The CreateNoWindow Boolean property instructs the Process class not to display a window when executing the program. This is good when you need to run a console process in the background and you don’t want that ugly black screen of the MS-DOS console to pop up. To make sure that the console window doesn’t show up, you should have the CreateProcess API start the process by setting the UseShellExecute property to false.

   Dim info As New ProcessStartInfo   info.Arguments = args   info.FileName = exe   info.CreateNoWindow = True   info.UseShellExecute = False   Dim p As New Process    p.StartInfo = info   p.Start()

Each file in Windows has a context menu, and each context menu has a set of predefined verbs like Open and Print. Windows defines the verbs based on the type of the file (that is, the extension) in the system registry. In the end, a verb is a string; the Verbs property returns an array of strings filled with the verbs found for the type of the file set to the FileName property. Each file type has a default verb?normally Open. When you start a process through the shell (see UseShellExecute), Windows executes its default verb. As I mentioned, in most cases this verb is Open and either the program runs or the document opens. However, be aware that this is a configurable parameter.

Printing from the Shell
You can exploit the built-in support for shell verbs to enrich your applications with functions exposed by other programs. For example, suppose you need to print a text file. .NET has simplified nearly all operations compared to the Win32 platform. Printing is, perhaps, one of the very few exceptions. Even in .NET, writing the code that prints a document is a boring task. If you aren’t too pretentious, you can content yourself with the printing engine in Notepad. The Notepad executable accepts the /p switch on command line and prints the specified document.

The shell API provides an interesting shortcut for printing text documents. The .NET Process API fully supports this shortcut through the Verb property. The code snippet below demonstrates how to print a text file using the print verb of the program registered to handle .txt files. By default, this program is notepad.exe.

   Dim p As New Process   Dim info As New ProcessStartInfo   info.FileName = "test.txt"   info.Verb = "print"   p.StartInfo = info   p.Start()

This snippet works regardless of which program you have registered on your computer to handle text files. In no way is it dependent on notepad.exe. It fails only if, for some reason, you don’t have a program associated with text files. The Verbs collection returns the list of all custom verbs defined on the specified file. A feasible value for the Verb property is any string contained in the Verbs collection.

Note that when you invoke a verb, Windows launches the associated program and has it do some work on the specified file. This means that the text file will first be opened in notepad.exe (or whatever program is associated with .txt files) and then automatically printed using the user interface of the program. If you use verbs, you must ensure that you set UseShellExecute to True.

Capturing Process Output
A console program gets its input from the command line or the standard console input, and sends its output to the standard console output. In addition, it automatically sends error messages to a separate output stream.

Based on the Win32 API capabilities, you can replace the input, output, and error streams with custom streams. For example, suppose you use the following syntax from the console prompt:

   program ?h >dump.txt

Any output the executable will generate is automatically redirected to the specified text file. The .NET process API supplies the same functionality through an object-oriented and easy-to-use set of properties. Let’s see how to capture the output of a process to a string.

To begin, you must ensure that you’ve set the RedirectStandardOutput property to True. The default value is False. In the case of redirected output, the Process class accumulates the output into a memory stream instead of just sending it out to the console output. You can then access any byte output through the Process class’ StandardOutput property. The following code snippet opens the console box, executes a dir statement, and captures the output. Figure 2 shows the result.

?
Figure 2: The output of a MS-DOS dir statement captured to a string and displayed through a MsgBox call.
   Dim p As New Process   Dim info As New ProcessStartInfo   info.UseShellExecute = False   info.CreateNoWindow = True   info.FileName = "cmd.exe"   info.Arguments = "/c dir *.*"   info.RedirectStandardOutput = True   p.StartInfo = info   p.Start()   p.WaitForExit()   MsgBox (p.StandardOutput.ReadToEnd())

The file name is cmd.exe, which is the system executable for the console box. Notice that the file name and the arguments are separate entities to the Process class. Anything assigned to the FileName property is considered as the file name, including switches and parameters. The /c switch in the argument above indicates that the Command window must be closed immediately after the command terminates. Using the settings for UseShellExecute and CreateNoWindow guarantees that no black window will display, not even for a moment.

When the command has completed, the output stream, a memory stream object, contains all the bytes output by the program. You can read this stream using the classic API of a Stream object. For example, the ReadToEnd method reads all the contents of the stream to a string.

Is there any concrete advantage in running console applications in a hidden window? Well, that depends on what your console program does. If your console program provides an essential feature not otherwise exploitable then this trick sets an important milestone on the way to embedding that functionality into your applications. You know how to execute that program in an invisible window; you know how to configure it through its command line; you know how to capture its output. By adding a bit of parsing on the generated output you can easily figure out what the program did, its results, and return code. To demonstrate this possibility, I’ll build a small class for zipping files.

Wrapping Zip Functionalities
The .NET Framework doesn’t include any class to compress files. A lot of third-party vendors sell well-designed classes that plug into the .NET Framework and extend it with various flavors of file compression functionality. A few programs are available under various license agreements to zip and unzip files and folders. One of these is gzip, a command line tool. Another one is WinZip. Unlike gzip, WinZip is a GUI program and doesn’t lend itself very well for use as a background tool. From the WinZip Web site, though, you can download a couple of command line utilities?wzzip and wzunzip. The code snippet below briefly illustrates how to use these tools. (For more information, please refer to each tool’s respective documentation.)

   gzip source    wzzip source target    wzunzip ?vb source

The gzip utility creates a file with the same name as the source file plus a .gz extension. For example, if you try to zip sample.doc, the tool creates a file called sample.doc.gz. The wzzip utility compresses the source file and creates a new file with the specified name. Finally, the third command line lists the files contained in the specified zip file. Let’s combine these command lines with the .NET process API to build a new class for zipping files.

Listing 3 shows the source code of the Gzip method on the Compressor class. It takes two strings?source and target?and creates a new file by zipping the source using the gzip utility. For the code to work, the gzip utility must be available in a publicly accessible directory. (For example, you can place gzip in the same directory as the sample program or the Windows directory.)

All instances of the Compressor class share the Gzip method. Calling the Compressor class to zip a file is easy:

   Compressor.Gzip(source, target)

The gzip utility doesn’t produce any significant output to capture. Because the utility always adds a .gz extension to the source file, I added some extra code to rename the compressed file to the specified name. The Run method encapsulates the logic needed to configure and invoke the Process class.

Listing 4 shows the source code of the Zip method. It uses the wzzip utility to create a familiar .zip file. Note that a .gz file is recognized and successfully handled by WinZip, but not by the default Windows XP extension for zip files. The Zip method is simply a wrapper around the Run method (Listing 3).

?
Figure 3: The original output of the wzunzip utility.
   Compressor.Zip(source, target)

The Zip method compresses the source file using the zip algorithm and creates a new file (the target). If the source parameter references a folder, Compressor will zip all the contents (files and subfolders).

The ReadZip method demonstrates how useful capturing output can be. The ReadZip method invokes the wzunzip utility with the ?vb switch and lists all the files contained in the specified zip file. The method first captures the output and then parses it to build a DataTable object. Figure 3 shows the original output of the wzunzip utility.

The parsing code depends on the version of the utility you use and it could break if you use a new version of WinZip with a different output schema. This example diagram skips heading and trailing lines and it breaks each line in the listing into pieces using a regular expression to detect blank strings. Note that extracting the file name is a little tricky because a file name can contain blanks. For this reason, my utility retrieves the file name by position. Figure 4 shows the same output post-processed to a DataTable and bound to a DataGrid.

?
Figure 4: The output of the wzunzip utility parsed and transformed into a DataTable.

Microsoft designed the process API in the .NET Framework to make programming easier. For many system level tasks, the .NET Framework still heavily relies on the capabilities and the functions of the operating system and the Win32 API. The process API is no exception. However, the abstraction layer that the .NET Framework builds unifies all the various Win32 API functions (CreateProcess, ToolHelp, shell) into a single object-oriented API. I think that programming objects with the Process class is far easier than programming quirky functions with lots of pointers and parameters. Don’t you agree?

   regsvr32.exe /u zipfldr.dll 
devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist