lthough .NET makes some things more complicated, launching external programs is not one of them. In classic VB, you used the Shell function to launch an application when you passed an executable file name. When you passed a data file name, VB opened the data file in its associated application. You could also control the window style of the launched application with an optional windowstyle parameter. For example, in VB6, the following line would launch the default text editor (usually NotePad) and open the file “c:somepathsomefile.txt“:
returnID = Shell("c:somepathsomefile.txt", _ vbNormalFocus)
With .NET things have beome much simpler. The System.Diagnostics namespace exposes a Process class that you can use to launch external programs. At the simplest level, you can launch a new process with the shared Process.Start method, passing it either the name of an executable file or a filename with an extension associated with an executable application. For example, the following code launches the “c:somepathsomefile.txt” file:
System.Diagnostics.Process.Start _ ("c:somepathsomefile.txt")
The Start method has an overloaded version that returns a Process object, so you can obtain a reference to the launched process, and use it for various purposes:
Dim myProcess As Process = System.Diagnostics.Process.Start ("c:somepathsomefile.txt") MessageBox.Show(myProcess.ProcessName)
At first glance, you seem to have lost the ability to control the window style (remember the second parameter from the Shell function?), but you haven’t. In many cases, you won’t need to set the window style explicitly, because the default is to display the launched process in a normal window with the focus (ProcessWindowStyle.Normal). But when you want to use a different window style, an overloaded Process.Start method accepts a ProcessStartInfo object parameter rather than a simple string. To use it, first create a ProcessStartInfo object and then set process initialization values. Two overloaded methods let you set a filename or a filename and a set of command-line parameters. And the ProcessStartInfo object also has a WindowStyle property, which consists of values from the System.Diagnostics.Process.WindowStyle enumeration. So you can call the Process.Start method and pass a ProcessStartInfo object to control the launched window’s style.
Dim psInfo As New _ System.Diagnostics.ProcessStartInfo _ ("c:somepathsomefile.txt") psInfo.WindowStyle = _ System.Diagnostics.ProcessWindowStyle.Normal Dim myProcess As Process = _ System.Diagnostics.Process.Start(psInfo)
Because the Process class exposes a StartInfo property that’s a ProcessStartInfo object, another way to accomplish the same result is to create a Process object and set its StartInfo properties. When you use a pre-created Process object, you can simply call its Start method rather than the Process class’s shared Start method.
Dim myProcess As System.Diagnostics.Process = _ new System.Diagnostics.Process() myProcess.StartInfo.FileName = _ "c:somepathsomefile.txt" myProcess.StartInfo.WindowStyle = _ System.Diagnostics.ProcessWindowStyle.Normal myProcess.Start
Setting Process Parameters at Design Time
The .NET framework ships with a Process component that encapsulates all this code at design time. You can find it in the Components area of the Toolbar. To use it, drag a Process component onto your form and then expand the StartInfo property in the Properties window, and set the StartInfo values to your liking (see Figure 1).
|Figure 1: You can add a Process component to a form that lets you set properties at design time rather than at run time.|
Monitoring Launched Processes
So far, the launched processes you’ve seen behave in an asynchronous manner; just like the classic VB Shell function. In other words, after launching the process, code in the parent program continues to execute. You need some way to monitor the launched process and find out when it exits?or sometimes, whether it’s still running. Depending on your application, you may need to approach the problem in any of several different ways.
- You want to launch the process, halting your program until it exits.
- You want to launch the process, monitor it, and do something only when it ends, letting your program run normally in the meantime.
- You want to launch the process, give it some input, let it process the input, and then force it to exit.
- You want to launch the process and do something only as long as the launched process is running or is running without problems. If the process exits or stalls, you want to take some action.
- You want to launch the process and give it some specific input, and/or retrieve the output for further processing. For example, you might want to launch a command window, programmatically type something into the window, and then retrieve and process the output.
Launch a Process and Wait Until It Exits
The simplest way to wait for a launched process to end is to call the Process.WaitForExit method. That causes the launching process to stop executing until the launched process exits. Unfortunately, when you use this directly from a Windows Form, it also causes the form to stop responding to system events, such as Paint.
So you wouldn’t normally want to use the WaitForExit method to launch an external program from a Button (although it’s perfectly appropriate to use the WaitForExit method to launch a second process from an application that has no visible user interface, such as calling a console application from the server in an ASP.NET application). The sample form has a button called “Launch and WaitForExit” (see Figure 2) that lets you see what happens when you use this method from a form.
|Figure 2: The sample form lets you test and experiment with various ways of launching a process.|
Private Sub btnWaitForExit_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnWaitForExit.Click ' create a new process Dim myProcess As Process = _ System.Diagnostics.Process.Start("sample.txt") ' wait until it exits myProcess.WaitForExit() ' display results MessageBox.Show("Notepad was closed at: " & _ myProcess.ExitTime & "." & _ System.Environment.NewLine & "Exit Code: " & _ myProcess.ExitCode) myProcess.Close() End Sub
The preceding example illustrates an interesting point. Even though the launched process has closed, you still have the ability to access the Process object in code; however, at that point, most of the Process properties are unavailable, because the process itself no longer exists. You can still read the ExitCode and ExitTime properties, which return Integer and DateTime values. DOS commands set an exit code that lets you know whether errors occurred. .NET and other Windows applications can set the value by using the return value of the main method. By default, the value is zero. For DOS commands, a non-zero ExitCode value indicates either an error or that the command process was closed abnormally.
|Author’s Note: When you use the Process.Start method from a process instance, you should also call Process.Close after the process exits, to free the memory associated with the Process object.|
Launch Invisible Processes
You don’t have to launch a process in a visible window; sometimes you just want to run a process and retrieve the output. The following example changes to the System folder, and then runs a DOS dir command with the file specification “*.com” which gives you a directory listing of the files in that folder with a .com extension. On Windows XP, the command shell interpreter recognizes the “&&” operator as a command separator, so you can place multiple commands on a single line. The “>>” operator redirects output into a named file. In this case, the code pipes the dir command results into the file “dirOutput.txt” in the path designated by the Application.StartupPath property.
Dim myProcess As Process = New Process() Dim s As String Dim outfile As String = Application.StartupPath & _ "dirOutput.txt" ' get the System path Dim sysFolder As String = _ System.Environment.GetFolderPath _ (Environment.SpecialFolder.System) ' set the file name and the command line args myProcess.StartInfo.FileName = "cmd.exe" myProcess.StartInfo.Arguments = "/C cd " & _ sysFolder & " && dir *.com >> " & Chr(34) & _ outfile & Chr(34) & " && exit" ' start the process in a hidden window myProcess.StartInfo.WindowStyle = _ ProcessWindowStyle.Hidden myProcess.StartInfo.CreateNoWindow = True myProcess.Start() ' if the process doesn't complete within ' 1 second, kill it myProcess.WaitForExit(1000) If Not myProcess.HasExited Then myProcess.Kill() End If ' display exit time and exit code MessageBox.Show("The 'dir' command window was " & _] "closed at: " & myProcess.ExitTime & "." & _ System.Environment.NewLine & "Exit Code: " & _ myProcess.ExitCode) myProcess.Close()
The preceding code returns an ExitCode value of zero (0). To see an example of a non-zero ExitCode, append an “X” or some other character to the System folder path to make it invalid. That causes an error, and the ExitCode value will be different. Because a process with an error could potentially run forever, the code uses an overloaded WaitForExit method that accepts a number of milliseconds to wait before returning control to the launching program. The preceding code waits for 1 second before ending the launched process by calling the Kill method, which forces the process to exit. Check for the existence of the dirOutput.txt file in your application’s startup directory to see the results.Detecting When a Process Exits
In VB6, you could call the Win32 API’s GetModuleUsage() function to determine when the process ended. The.NET equivalent is to loop repeatedly after launching the process, checking the Process.HasExited property and calling the Application.DoEvents method to handle other events in your application until the process ends.
Do While Not myProcess.HasExited
But the Process class gives you a cleaner way to determine when the process exits?it can raise an Exited event. To make this happen, you need to set the Process.EnableRaisingEvents property to True (by default, the property is False), and create an event handler. For example:
' allow the process to raise events myProcess.EnableRaisingEvents = True ' add an Exited event handler AddHandler myProcess.Exited, _ AddressOf Me.ProcessExited ' start the process myProcess.Start() ' event handler Friend Sub ProcessExited(ByVal sender As Object, _ ByVal e As System.EventArgs) Dim myProcess As Process = DirectCast( _ sender, Process) MessageBox.Show("The process exited, raising " & _ "the Exited event at: " & myProcess.ExitTime & _ "." & System.Environment.NewLine & _ "Exit Code: " & myProcess.ExitCode) myProcess.Close() End Sub
One potential problem with both of these methods is that if the launched process hangs or never exits, your application is stuck. One solution is to add a timer that fires periodically, and checks to see if the launched application is still responding.Controlling Process I/O
Sometimes you want to go beyond a simple command line and send more complex input directly to a launched process. Similarly, piping the output to a file, as in the preceding example, is not always the best option. It many cases, it’s much more efficient to pipe the output directly back to your program. For programs that use StdIn, StdOut, and StdErr, such as console applications, you can override the defaults and provide a StreamWriter to write input and StreamReaders to read the StdOut and StdErr outputs. To do that, when you launch the process, you set the ProcessStartInfo object’s RedirectStandardInput, RedirectStandardOutput, and RedirectStandardError properties to True. Then, after launching the process, use the Process object’s StandardInput, StandardOutput, and StandardError properties to assign the I/O streams to StreamReader and StreamWriter objects.
One caveat: By default, the framework uses the Win32 ShellExecute function internally to launch processes (that’s how it can automatically launch the appropriate application based on the file association); but when you want to reassign the I/O streams, you must set the ProcessStartInfo.UseShellExecute property to False before starting the process. Note that when you do that, you must either specify the full path to the file, or the file location must be in the environment path string or in one of the places Windows searches for files. For example, the following code creates an invisible window, retrieves a directory listing of the .com files in the System folder, and then displays the results in a MessageBox.
Dim myProcess As Process = New Process() Dim s As String myProcess.StartInfo.FileName = "cmd.exe" myProcess.StartInfo.UseShellExecute = False myProcess.StartInfo.CreateNoWindow = True myProcess.StartInfo.RedirectStandardInput = True myProcess.StartInfo.RedirectStandardOutput = True myProcess.StartInfo.RedirectStandardError = True myProcess.Start() Dim sIn As StreamWriter = myProcess.StandardInput sIn.AutoFlush = True Dim sOut As StreamReader = myProcess.StandardOutput Dim sErr As StreamReader = myProcess.StandardError sIn.Write("dir c:windowssystem32*.com" & _ System.Environment.NewLine) sIn.Write("exit" & System.Environment.NewLine) s = sOut.ReadToEnd() If Not myProcess.HasExited Then myProcess.Kill() End If MessageBox.Show("The 'dir' command window was " & _ closed at: " & myProcess.ExitTime & "." & _ System.Environment.NewLine & "Exit Code: " & _ myProcess.ExitCode) sIn.Close() sOut.Close() sErr.Close() myProcess.Close() MessageBox.Show(s)
For programs that don’t use StdIn, you can use the SendKeys method to input keystrokes. For example, this code launches Notepad and enters some text.
Dim myProcess As Process = New Process() myProcess.StartInfo.FileName = "notepad" myProcess.StartInfo.WindowStyle = _ ProcessWindowStyle.Normal myProcess.EnableRaisingEvents = True AddHandler myProcess.Exited, _ AddressOf Me.SendKeysTestExited myProcess.Start() ' wait until the program is ready for input myProcess.WaitForInputIdle(1000) If myProcess.Responding Then System.Windows.Forms.SendKeys.SendWait( _ "This text was entered using the " & _ "System.Windows.Forms.SendKeys method.") Else myProcess.Kill() End If
You can send any keystroke using the SendKeys method, including Alt, Ctrl, and Shift keys, which require special symbols; therefore, you can use it to save or load files, exit or perform other menu-driven commands. However, the SendKeys method only sends keys to the active window (the one that has the focus), so it can cause problems if an application loses the focus during the process. (Check the MSDN documentation topic “Sendkeys class” for more information
Be sure to give the launched process time to create its main window and display before sending keystrokes. The Process.WaitForInputIdle method causes the launching application to wait until the launched process is in an idle state, waiting for user input. The parameter is an Integer timeout value, in milliseconds. In this case, the launching program waits up to one second for the text editor to be ready for input. If by that time the program is not ready, it tests to see if it’s responding before continuing, otherwise it kills the process. For some systems or applications, you would want to set the parameter to a higher value, because not all processes launch as quickly as Notepad. If you omit the timeout parameter, and the launched process never enters an idle state, your program could wait forever.
To sum up, although the Shell function is still available, the System.Diagnostics.Process class gives you more control over launching and controlling processes, as well as detecting when they exit, and getting an exit code. By redirecting StdIn, StdOut, and StdErr, you can send and receive data from applications. Using the SendKeys method, you can send input to applications that don’t use StdIn, and you can force them to save data to files, where you can later read the saved data from your application.