.NET and running processes

This article was originally published in VSJ, which is now part of Developer Fusion.
Back in the days of VB 6.0, getting information about processes running on your system required a lengthy series of API calls and a lot of code. With the advent of the .NET Framework, getting system information can be performed in an object-oriented manner using the Process Class in the System.Diagnostics namespace. This article will show how to develop a simple program that will use the Process Class and the Treeview control to display module, memory and thread information about running processes.

Classes vs APIs

When I first began familiarising myself with the .NET Framework, I was quite disturbed to find that one of my favorite add-ins – the API viewer – was missing. As anyone who has tried calling APIs from VB.NET soon learns, you can't just copy and paste the Declare statements from VB 6.0 into VB.NET and have any hope that they will work. I have always been a heavy user of APIs, and the prospect of no longer having this handy tool at my disposal was not giving me a very good first impression of the direction Microsoft had planned for VB.

However, as I soon discovered, I was missing the point entirely. APIs, while not exactly obsolete, are no longer as important under .NET as they were for VB 6.0 developers. This is because many of the functions formerly provided by the APIs are now supplied by one or more classes in the .NET framework. This provides for an object-oriented interface analogous to using a TextBox control instead of the TextBox APIs.

A bit about the GUI

The sample program presented here is an application that cries out for the TreeView control. The application will list each process running on the system as a separate node. Below each of these nodes will be additional nodes for Memory, Module and Thread information. Before we get started with the data, let's take a quick look at the TreeView control and how to use it to build a tree structure.

Most of the action with a TreeView control happens with its Nodes collection. You don't really add Nodes to a TreeView control, rather you add a Node to its Nodes collection:

VB.NET:

TreeView1.Nodes.Add("Hello World")
C#:
treeView1.Nodes.Add("Hello world");
Actually, the Add method as shown above returns the Node just added which is handy if you wish to do something with the Node programmatically after it has been added:

VB.NET:

Dim nodNew As TreeNode = _
	TreeView1.Nodes.Add("Hello World")
nodNew.ForeColor = _
	System.Drawing.Color.Red
C#:
TreeNode nodNew =
	treeView1.Nodes.Add("Hello World");
nodNew.ForeColor =
	System.Drawing.Color.Red;
The Add method is overloaded to also allow adding an existing Node to the Nodes collection:

VB.NET

Dim nodExisting As New _
	TreeNode("Existing Node")
TreeView1.Nodes.Add(nodExisting)
C#
TreeNode nodExisting =
	new TreeNode("Existing Node");
treeView1.Nodes.Add(nodExisting);
Used in this form, the return value is the index of the node just added rather than the node itself. The important thing about this construct is that the Node being added may itself have additional Nodes, sub-Nodes and more sub-Nodes. We will take advantage of this fact, as you will see later.

What exactly is a Process?

The easiest way to view the processes running on your system is to press Ctrl-Alt-Delete to bring up the Windows Task List. Press the Processes tab and you will be presented with a list of all the processes running on your system. Every process has a unique Process ID, and a specific allocation of resources on the system.

A process may have zero or more windows associated with it. All processes consist of one or more threads. A thread can be thought of as a stream of executing code. Although it is not possible to create a multi-thread process using Visual Basic 6.0, it is indeed possible to create multiple threads with VB.NET.

Another important attribute of a process is the collection of modules associated with that process. For example, every Visual Basic 6.0 program requires the VB 6.0 Run-time .exe as well as a number of DLLs. Obtaining proper module information is an important part of designing a deployment strategy or trouble-shooting broken COM objects.

A Look at the Process Class

The .NET Process Class is found in the System.Diagnostics namespace. This class is very easy to use, but contains a number of collections which may seem a bit intimidating at first glance. Let's take a look at the properties and methods we'll be using in this example.

If you want to look at the processes running on your system, the most important method provided by the Process Class is the GetProcesses method. As the name implies, this method will return an array of Process Class objects. In other words we use a Process Class to get the array of Process Class objects. We can declare and populate the array and retrieve the list of processes with a single line of code:

VB.NET

Dim p() As System.Diagnostics.Process_
	= System.Diagnostics.Process._
		GetProcesses()
C#
System.Diagnostics.Process[] p
	= System.Diagnostics.Process.GetProcesses();
Once populated, each element in the array represents a single process running on the system. We can iterate through the list of processes by traversing the array:

VB.NET

For i = 0 To p.GetUpperBound(0)
	...
C#
for (i = 0; i<=p.GetUpperBound(0); i++) {
	...
While the GetProcesses method returns all processes currently running on the system, you can get an instance of a single process as well. The GetCurrentProcess will return a Process Object representing the process in which it is running. You can also get an instance of a process based on its ProcessName or ID using the GetProcessesByName and GetProcessByID methods. Since there can be more than one process running on the system with the same name, the GetProcessByName method returns an array, while the GetProcessByID always returns a single process, since the ID will always be unique:

VB.NET

Dim p1 As System.Diagnostics.Process _
	= System.Diagnostics.Process.GetCurrentProcess()
Dim p2 As System.Diagnostics.Process() = _
	System.Diagnostics.Process.GetProcessesByName( _
	"NOTEPAD")
Dim p3 As System.Diagnostics.Process = _
	System.Diagnostics.Process.GetProcessById(15)
C#
System.Diagnostics.Process p1
	= System.Diagnostics.Process.GetCurrentProcess();
System.Diagnostics.Process[] p2 =
	System.Diagnostics.Process.GetProcessesByName(
	"NOTEPAD");
System.Diagnostics.Process p3
	= System.Diagnostics.Process.GetProcessById(15);
Once you have an instance of a process, getting the memory information is quite straightforward. There are several properties to return the memory usage information, and their names are self-explanatory:
  • NonpagedSystemMemorySize
  • PagedMemorySize
  • PagedSystemMemorySize
  • PeakPagedMemorySize
  • PeakVirtualMemorySize
  • PeakWorkingSet
  • VirtualMemorySize
  • WorkingSet
None of these properties is supported under Windows 98, and trying to access any of them will cause a PlatformNotSupported exception, so you should wrap any code that accesses any of these properties inside a Try/Catch block:

VB.NET

Dim p As System.Diagnostics.Process _
	= System.Diagnostics.Process._
	GetCurrentProcess()
Try
	Debug.WriteLine( _
	p.NonpagedSystemMemorySize._
	ToString())
Catch
	System.Diagnostics.Debug._
	WriteLine("Not supported.")
End Try
C#
System.Diagnostics.Process p =
	System.Diagnostics.Process.
	GetCurrentProcess();
try {
	System.Diagnostics.Debug.WriteLine(
		p.NonpagedSystemMemorySize.
		ToString());
}
catch {
	System.Diagnostics.Debug.WriteLine(
	"Not supported.");
}
Actually, a number of properties of the Process Class may cause exceptions when run under Windows 98 or for other reasons. Whenever using this or any other .NET class, it is a good idea to read the on-line help text, which will list all members of the class, and any exceptions that may occur.

Now, let's look at something that will take a bit more work. The Modules property of a Process Class returns an array of ProcessModules – one for each module, typically a DLL or EXE file – associated with the process. With this class, we can determine the module name using the ModuleName property, the FileName with the FileName property and version information using the FileVersionInfo property. This last one is itself another object, containing the Version Number, Comments, Company Name, Copyright – and other related items. The Count property is used to iterate through the list:

VB.NET

With SomeProcess
	For i = 0 To .Modules.Count - 1
		Debug.WriteLine( _
			.Modules.Item(i).ModuleName)
		Debug.WriteLine _
		(.Modules.Item(i)._
		FileVersionInfo.ProductVersion)
		Debug.WriteLine _
		(.Modules.Item(i)._
		FileVersionInfo.Comments)
	Next
End With
C#
for(i = 0; i <
	SomeProcess.Modules.Count; i++) {
	System.Diagnostics.Debug.WriteLine(
	SomeProcess.Modules[i].ModuleName);
	System.Diagnostics.Debug.WriteLine(
	SomeProcess.Modules[i].
	FileVersionInfo.ProductVersion);
	System.Diagnostics.Debug.WriteLine(
	SomeProcess.Modules[i].
	FileVersionInfo.Comments);
}
Obtaining the thread information is similar to the module information. We use the Threads property of the Process Class, which returns an array of ProcessThread class elements. Each of these contains a number of properties related to the thread's status and priority. We will include a few of these values in our sample Process Viewer program.

So far we've been looking at getting information about already running processes. We can also use the Process Class to start our own processes as well. The Start method is used to initiate a process and is similar to the old VB 6.0 Shell command with one important difference. The Start method will accept not only a program path name, but any file name with a registered extension can be passed as well.

Before using the Start method, you must first indicate the file to run or open. This is done using the StartInfo property – itself another class. Once we have set up the FileName property, we can start the process:

VB.NET

Dim p As New _
	System.Diagnostics.Process()
p.StartInfo.FileName = "Calc.exe"
p.Start()
p.StartInfo.FileName = _
	"c:\WinNT\Win.ini"
p.Start()
C#
System.Diagnostics.Process p =
	new System.Diagnostics.Process();
p.StartInfo.FileName = "Calc.exe";
p.Start();
p.StartInfo.FileName =
	"c:\\WinNT\\Win.ini";
p.Start();
Actually, the Start method is overloaded to allow passing the FileName as a parameter, but this will not leave the Process object associated with the process after it has been launched, so the above syntax is preferred.

Once the process has been launched, you can cause an event to fire in your application when the process has ended. This is very handy in cases where you are interfacing with a third party application and wish to call another program and wait for it to terminate before continuing. To associate an event with the termination of the process, you must first set the EnableRaisingEvents property to true. Next, an Event Handler is associated with the Exited event using the AddHandler method for VB.NET or the += operator with C#:

VB.NET

p.EnableRaisingEvents = True
AddHandler p.Exited, _
	AddressOf Process_Exited
C#
p.EnableRaisingEvents = true;
p.Exited += new
System.EventHandler(Process_Exited);
The above example assumes that you have written a routine called Process_Exited, with the correct signature for the Exited event of a Process Class:

VB.NET

Private Sub Process_Exited(ByVal _
	sender As Object, _
	ByVal e As System.EventArgs)
	Dim p As _
		System.Diagnostics.Process = _
		CType(sender, _
		System.Diagnostics.Process)
	MsgBox(p.StartInfo.FileName + _
		" has exited.")
	p.Dispose()
End Sub
C#
private void Process_Exited(object
	sender, System.EventArgs e) {
	System.Diagnostics.Process p =
		(System.Diagnostics.Process)
		sender;
	MessageBox.Show(
		p.StartInfo.FileName +
		" has exited.");
	p.Dispose();
}

A Process Viewer

Now that we are familiar with the Process Class, let's design a class that will gather the actual pieces of the puzzle needed to solve this problem. As we will see, the process class takes care of all the work needed to grab the running processes and their associated information. The real work in this application is with the presentation layer. We will be taking information from the Process Class and arranging it into a hierarchy of TreeNode objects. With this in mind, we will write a simple class that will accept a process object and return the appropriate node structure for this piece of the process. Once constructed, this node will be appended to the appropriate node of the TreeView control. Thus, the GUI will need only request a particular nodes list, and the class will do the rest.

Since we have a pretty good idea of what we want our class to do, let's take a top down approach and look at the main loop that will load the TreeView control first. Having populated our process array (see above) we will iterate through each process, adding a TreeNode to the TreeView control for each process, using the ProcessName as the Text for the node. To this node, we will add three more nodes – one each for the Memory, Module and Thread information for this process. In the code below, pnNodes refers to an instance of our ProcessNodes class, which we will develop shortly:

VB.NET

' Loop for each process
For i = 0 To p.GetUpperBound(0)
	' Use the Process Name as the Top
	' Level Node.
	' nodNewNode = tvProcesses.Nodes._
	' Add(p(i).MainWindowTitle)
	Debug.WriteLine(p(i)._
		ProcessName.ToString)
	nodNewNode = _
		tvProcesses.Nodes.Add(p(i)._
		ProcessName)

	' To this node, add the Memory
	' Usage, Module and Thread nodes...
	' ...which will also contain
	' additional nodes as required.
	nodNewNode.Nodes.Add(_
	pnNodes.GetMemoryUsageNode(p(i)))
	nodNewNode.Nodes.Add(_
		pnNodes.GetModulesNode(p(i)))
	nodNewNode.Nodes.Add(_
		pnNodes.GetThreadsNode(p(i)))
Next
C#
// Loop for each process.
for (i = 0; i <= p.GetUpperBound(0);
i++) {
	// Only show processes with a
	// windows title.
	// Use the Process Name as the top
	// level node.
	nodNewNode = tvProcesses.Nodes.Add(
		p[i].ProcessName);

	// To this node, add the Memory
	// Usage, Module and Thread
	// nodes which will also contain
	// additional nodes as required.
	nodNewNode.Nodes.Add(
		pnNodes.GetMemoryUsageNode(
		p[i]));
	nodNewNode.Nodes.Add(
		pnNodes.GetModulesNode(p[i]));
	nodNewNode.Nodes.Add(
		pnNodes.GetThreadsNode(p[i]));
}
Now, all that's left is to create a class that will fill in the missing pieces. To add a class to the project, select Add Class from the Project Menu in the IDE. We will name the class as clsProcessNodes. We will write GetMemoryUsageNode, GetModulesNode and GetThreadsNode methods, each of which will return a TreeNode. We will wrap any problem properties inside a Try/Catch block. The simplest of these is GetMemoryUsageNode. See below for this code. (Remember that this information will not be supported for Windows 98.)

VB.NET

' GetMemoryUsageNode: Returns a TreeNode containing the
' memory information for a process.
Public Function GetMemoryUsageNode (ByVal ThisProcess _
	As 	System.Diagnostics.Process) As TreeNode

	' Create the main threads node, which will be returned
	' by this function. We will create a tree structure
	' below this node representing the threads
	' information.
	Dim tnMemoryNode As New TreeNode("Memory Usage")

	' Add additional nodes containing memory usage
	' information.
	Try
		With ThisProcess
			tnMemoryNode.Nodes.Add( _
				"Non Paged System Memory Size: " + _
				.NonpagedSystemMemorySize.ToString)
			tnMemoryNode.Nodes.Add("Paged Memory Size: " _
				+ .PagedMemorySize.ToString)
			tnMemoryNode.Nodes.Add( _
				"Paged System Memory Size: " + _
				.PagedSystemMemorySize.ToString)
			tnMemoryNode.Nodes.Add(
				"Peak Paged Memory Size: " + _
				.PeakPagedMemorySize.ToString)
			tnMemoryNode.Nodes.Add( _
				"Peak Virtual Memory Size: " + _
				.PeakVirtualMemorySize.ToString)
			tnMemoryNode.Nodes.Add("Peak Working Set: " _
				+ .PeakWorkingSet.ToString)
			tnMemoryNode.Nodes.Add( _
				"Virtual Memory Size: " + _
				.VirtualMemorySize.ToString)
			tnMemoryNode.Nodes.Add("Working Set: " _
				+ .WorkingSet.ToString)
		End With
	Catch
		tnMemoryNode.Nodes.Add( _
			"Memory Info: (not supported)")
	End Try
	GetMemoryUsageNode = tnMemoryNode
End Function
C#
// GetMemoryUsageNode: Returns a TreeNode containing the
// memory information for a process.
public TreeNode GetMemoryUsageNode(
System.Diagnostics.Process ThisProcess) {

	// Create the main threads node, which will be
	// returned by this function. We will create a tree
	// structure below this node representing the threads
	// information.
	TreeNode tnMemoryNode = new TreeNode("Memory Usage");

	// Add additional nodes containing memory usage
	// information.
	try {
		tnMemoryNode.Nodes.Add(
		"Non Paged System Memory Size: " +
		ThisProcess.NonpagedSystemMemorySize.ToString());
		tnMemoryNode.Nodes.Add("Paged Memory Size: "
		+ ThisProcess.PagedMemorySize.ToString());
		tnMemoryNode.Nodes.Add("Paged System Memory Size: "
		+ ThisProcess.PagedSystemMemorySize.ToString());
		tnMemoryNode.Nodes.Add("Peak Paged Memory Size: "
		+ ThisProcess.PeakPagedMemorySize.ToString());
		tnMemoryNode.Nodes.Add("Peak Virtual Memory Size: "
		+ ThisProcess.PeakVirtualMemorySize.ToString());
		tnMemoryNode.Nodes.Add("Peak Working Set: "
		+ ThisProcess.PeakWorkingSet.ToString());
		tnMemoryNode.Nodes.Add("Virtual Memory Size: "
		+ ThisProcess.VirtualMemorySize.ToString());
		tnMemoryNode.Nodes.Add("Working Set: "
		+ ThisProcess.WorkingSet.ToString());
	}
	catch {
		tnMemoryNode.Nodes.Add(
			"Memory Info: (not supported)");
	}
	return(tnMemoryNode);
}
GetModulesNode and GetThreadsNode are a little bit more involved. Each of these methods will deal with an array of objects, and so we will be adding another TreeNode for each element rather than just a scalar value as we did with GetMemoryUsage. Let's take a look at the GetModulesNode routine. The GetThreadsNode routine is very similar and not listed here. First, we declare a TreeNode, which will be returned by the function to represent all the module information. Next we iterate through the Modules collection. For each module, we add a new node to the one just declared, appending additional nodes for each module property being displayed – in this example the file name, version information and comments:

VB.NET

' GetModulesNode: Returns a TreeNode containing
' the modules information for a process.
Public Function GetModulesNode(ByVal ThisProcess As _
	System.Diagnostics.Process) As TreeNode

	' Create the main modules node, which will be returned
	' by this function. We will create a tree structure
	' below this node representing the module information.
	Dim tnModulesNode As New TreeNode("Modules")
	' Loop for each module used by this process.
	Try
		With ThisProcess.Modules
			Dim i As Integer
			For i = 0 To .Count - 1
				' Add a node for this process using the
				' Module Name as the node text. We also
				' declare a TreeNode variable to receive
				' the results.
				Dim tnThisModule As TreeNode = _
				tnModulesNode.Nodes.Add(.Item(i).ModuleName)

				' tnThisModule has the node just added. To
				' this node, append additional nodes
				' containing file name, version & comments.
				tnThisModule.Nodes.Add("File name: " + _
					.Item(i).FileName)
				Try
					tnThisModule.Nodes.Add("Version: " + _
					.Item(i).FileVersionInfo.ProductVersion)
					tnThisModule.Nodes.Add("Comments: " + _
					.Item(i).FileVersionInfo.Comments)
				Catch
					tnThisModule.Nodes.Add( _
						"Version Info: (not available)")
				End Try
			Next
		End With
	Catch
		tnModulesNode.Nodes.Add( _
			"Module Info: (not available)")
	End Try

	' Return the main node.
	GetModulesNode = tnModulesNode
End Function
C#
// GetModulesNode: Returns a TreeNode containing the
// modules information for a process.
public TreeNode GetModulesNode(
	System.Diagnostics.Process ThisProcess) {

	// Create the main modules node, which will be
	// returned by this function. We will create a tree
	// structure below this node representing the
	// module information.
	TreeNode tnModulesNode = new TreeNode("Modules");
	// Loop for each module used by this process.
	try {
		int i = 0;
		for (i=0; i< ThisProcess.Modules.Count; i++) {
			// Add a node for this process using the Module
			// Name as the node text. We also declare a
			// TreeNode variable to receive the results.
			TreeNode tnThisModule =
				tnModulesNode.Nodes.Add(
				ThisProcess.Modules[i].ModuleName);

			// tnThisModule has the node just added. To
			// this node, append additional nodes
			// containing the file name, version & comments.
			tnThisModule.Nodes.Add("File name: " +
				ThisProcess.Modules[i].FileName);
			try {
				tnThisModule.Nodes.Add("Version: "
					+ ThisProcess.Modules[i].FileVersionInfo.
					ProductVersion);
				tnThisModule.Nodes.Add("Comments: " +
					ThisProcess.Modules[i].FileVersionInfo.
					Comments);
			}
			catch {
				tnThisModule.Nodes.Add(
					"Version Info: (not available)");
		}	}
	}
	catch {
		tnModulesNode.Nodes.Add(
			"Version Info: (not available)");
	}
	// Return the main node.
	return(tnModulesNode);
}
That's all there is to it. Getting information about processes running on your system or launching another process has never been so simple.


Jon Vote is an independent consultant based on the west coast of the USA. He is a Microsoft Certified Solution Developer (MCSD), with a degree in Computer Science from Southern Oregon University. He can be reached at jon@skycoder.com.
Both VB.NET and C# versions of the source code can be downloaded from www.skycoder.com/downloads.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“The most exciting phrase to hear in science, the one that heralds new discoveries, is not 'Eureka!' but 'That's funny...'” - Isaac Asimov