Have you ever heard of redirected input or consoleapplications? Have you ever had the need to launch MS-DOS programs, wait for them toterminate, and then dump their output to screen? Let’s see how to accomplish all thisleveraging Win32 and Visual Basic.
Outside the exciting and pretty ideal world of the just-releasedsoftware products, many developers have still to cope with some legacy MS-DOS program. Inmost cases, you have a Windows application that launches a MS-DOS program, waits for it toterminate and then has the need to capture its output, possibly as a string. Is all thisdoable with Visual Basic? But of course.
In this article, I’ll describe a CShell classthat is meant to be an enhanced wrapper for the glorious Shell command.It exposes an Execute method that runs650 368 2939 a batch-mode, non-interactiveprogram and returns any character sent out to the standard output device during execution.
Redirecting Output
Working with MS-DOS, how many times you entered a commandlike this to get the list of all the executables in the root of your disk C:?
C:> dir *.exe >Programs.txt
The final effect of such a command was toopen an I/O channel on a file device and work with it the same way it would have done withthe screen. We need just to reproduce this behavior from within a Win32 application.
The first approach that comes to mind suggests to employ the VB’sshell function to execute the program’s command line using the MS-DOS redirection operator> to send everything to a file. Let’s see how this should work. The shellfunction has the following syntax:
processID = Shell(pathname[, windowstyle])
The first argument is the command line to execute,while the second argument denotes the style of the window in which the program must run.The returned value is the ID of the process created, if any. Notice that the returnedvalue is the process ID and not the process handle (HPROCESS). You can obtain a HPROCESSfrom the ID using the OpenProcess() function. Shell starts any programasynchronously. In other words, the calling program continues to run as soon as the newprocess is started and nothing knows about its termination.
Under the Hood of the Shell Function
The Shell function utilizes the CreateProcess()function to start a new process. CreateProcess is the standard Win32 way to create a newprocess. It’s a really powerful function even if its prototype is a bit baffling:
Declare Function CreateProcess Lib"kernel32" _
Alias "CreateProcessA" ( _
ByVal lpApplicationName As String, _
ByVal lpCommandLine As String, _
lpProcessAttributes As Any, _
lpThreadAttributes As Any, _
ByVal bInheritHandles As Long, _
ByVal dwCreationFlags As Long, _
lpEnvironment As Any, _
ByVal lpCurrentDirectory As String, _
lpStartupInfo As STARTUPINFO, _
lpProcessInformation As PROCESS_INFORMATION _
) As Long
The program name you pass to Shellis in turn passed to CreateProcess as its lpApplicationName argument. Theredirection operator you might think to put in the command line is ignored byCreateProcess that simply has another way to achieve the same result. Thus you shouldforget a syntax like
Shell "MyProg.exe >File.txt"
and begin concentrating on the features CreateProcess()exposes.
Arranging a Call to CreateProcess
The following is a typical way to call into CreateProcess. Asyou can see, many arguments are just zeroes:
Dim pi As PROCESS_INFORMATION
Dim si As STARTUPINFO
bResult = CreateProcess( _
szProgram, _
vbNullString, _
ByVal 0&, _
ByVal 0&, _
True, _
NORMAL_PRIORITY_CLASS, _
ByVal 0&, _
vbNullString, _
si, _
pi)
The function returns a boolean value denoting the success of theoperation. The first two arguments indicate the program name and its command line. Youaren?t strictly required to split program name and command line but you could passall through the single lpApplicationName parameter. In most cases youdon?t need to take care of all the remaining arguments until the last two. (You canleave them to take the default values, as shown above.)
The core of CreateProcess ? at least for the purposes of thisarticle ? are the arguments of type STARTUPINFO and PROCESS_INFORMATION.
Public Const NORMAL_PRIORITY_CLASS = &H20
Public Const STARTF_USESTDHANDLES = &H100
Public Const STARTF_USESHOWWINDOW = &H1
Type PROCESS_INFORMATION
hProcess As Long
hThread As Long
dwProcessId As Long
dwThreadId As Long
End Type
Type STARTUPINFO
cb As Long
lpReserved As String
lpDesktop As String
lpTitle As String
dwX As Long
dwY As Long
dwXSize As Long
dwYSize As Long
dwXCountChars As Long
dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
lpReserved2 As Byte
hStdInput As Long
hStdOutput As Long
hStdError As Long
End Type
Any process has its own handle and its own ID. When a processstarts it generates at least one thread. Alos threads are identified via handles and IDs.All this information is returned through the parameter of PROCESS_INFORMATION.
The Standard Output Device
The program you run will create a window. To establish a channelwith this window you could utilize the STARTUPINFO argument. Such a data structure letsyou decide the style of the window, its size, the text for the title bar and even thedesktop (under Windows NT only). It also makes available three handles to manage the threestandard I/O devices: input, output and error. If you want the output of your MS-DOSprogram to go to a file just pass a handle to that file as the hStdOutfield.
Notice that this technique will work only with those program that send their output to the standard output device (aka stdout). This is quite usual with many MS-DOS and Win32 console programs, but is not a rule. |
To make use of the hStdOut field you need to addthe STARTF_USESTDHANDLES to the dwFlags field:
si.cb = Len(si)
si.dwFlags = STARTF_USESHOWWINDOW Or _
STARTF_USESTDHANDLES
I’ve said that you need to pass a file handle to hStdOut.That’s true but not all the handles will work fine. The only valid handle is any returnedby CreateFile(). There’s a great difference between, say, a window handleand what’s needed here.
Dim hFile As Long
hFile = CreateFile(
sTempFile, _
GENERIC_READ Or GENERIC_WRITE, _
0, _
ByVal 0&, _
CREATE_ALWAYS, _
0, _
ByVal 0&)
If hFile Then
si.hStdOutput = hFile
End If
Once you created your output file and passed its handle to hStdOuteverything that the running program sends out to its stdout goes to the file. At the endof the program you have just to close the file with CloseHandle() andread it back in case you want to return a string.
Designing the CShell Class
The CShell class I mentioned earlier will havejust an Execute method with a prototype like this:
Public Function Execute( _
ByVal szProgram As String, _
Optional ByVal fRedirectOutput As Boolean_
) As String
It takes the name of the program to run, including any neededparameters, and a boolean flag that can explicitly state whether output redirection isneeded or not.
Public Function Execute(ByVal szProgram As String, _
Optional ByVal fRedirectOutput) As String
Dim hProcess As Long
Dim hFile As Long
Dim si As STARTUPINFO
Dim pi As PROCESS_INFORMATION
Execute = ""
If IsMissing(fRedirectOutput) Then
fRedirectOutput = False
End If
' If redirection needed executes program
sTempFile = GetTempFile
If fRedirectOutput Then
si.cb = Len(si)
si.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
si.wShowWindow = SW_HIDE
' Creates a temp file
hFile = CreateFile(sTempFile, GENERIC_READ Or GENERIC_WRITE, _
0, ByVal 0&, CREATE_ALWAYS, 0, ByVal 0&)
If hFile Then
si.hStdOutput = hFile
End If
End If
' Creates the process
bResult = CreateProcess(szProgram, vbNullString, _
ByVal 0&, ByVal 0&, True,NORMAL_PRIORITY_CLASS, _
ByVal 0&, vbNullString, si, pi)
If bResult Then
WaitForSingleObject pi.hProcess, INFINITE
End If
' Closes handles
If hFile And fRedirectOutput Then
Dim numOfBytes As Long
Dim buf As String
numOfBytes = GetFileSize(hFile, ByVal 0&)
buf = Space(numOfBytes)
CloseHandle (hFile)
' Reads back the file content
hFile = lopen(sTempFile, 0)
lread hFile, buf, numOfBytes
lclose (hFile)
' Returns
Execute = buf
DeleteFile (sTempFile)
End If
End Function
Private Function GetTempFile() As String
Dim s As String
dwSize = GetTempPath(0, s)
s = Space(dwSize - 1)
GetTempPath dwSize - 1, s
If Right$(s, 1) <> "" Then
s = s + ""
End If
s = s + "TEMP.tmp"
GetTempFile = s
End Function
Often you have also the problem of stopping the calling processuntil the spawned process terminates. This can be easily accomplished via WaitForSingleObject()as shown in the listing above.
Summary
This article demonstrated how to capture the output of MS-DOSprogram that writes to the standard output device. By making a specialized use ofCreateProcess you can redirect the output to a temporary file and then read it back into astring. The source code available includes a cshell.cls file plus a cshell.baswith all the declarations needed.