Web Scripting
Active Server Pages
Java
Python
Online Tools
Click Here


Home

What's New

Articles

Code Downloads

Code Snippets

Message boards

Links
Tool Box

Books

Mailing List
Receive free code snippets and notices when this site is updated.

Contact

 

Tell a friend about this article

Capturing the Output of a shelled program

Redirecting the standard output and standard error

Capturing the output of a shell application can be useful, you could use it to create a better ms dos window or create a development environment that runs the compiler and returns any error messages.

DOS applications send their output to the standard output pipe and error messages to the standard error pipe.  Usually these pipes are directed to the screen so the user can see what's happening.

In order to capture the output of a shell dos application our application must redirect the stdOutput and stdErr pipes from the screen to a pipe that we create.

The function shown below will execute the application specified and return it's output.

'Runs an ms-dos application and returns
'text print to stdOutput and stdErr.
'This text would ussaly be printed to the screen.
Private Function ExecuteApp(sCmdline As String) As String
    Dim proc As PROCESS_INFORMATION, ret As Long
    Dim start As STARTUPINFO
    Dim sa As SECURITY_ATTRIBUTES
    Dim hReadPipe As Long 'The handle used to read from the pipe.
    Dim hWritePipe As Long 'The pipe where StdOutput and StdErr will be sent.
    Dim sOutput As String
    Dim lngBytesRead As Long, sBuffer As String * 256

    sa.nLength = Len(sa)
    sa.bInheritHandle = True
      
    ret = CreatePipe(hReadPipe, hWritePipe, sa, 0)
    If ret = 0 Then
        MsgBox "CreatePipe failed. Error: " & Err.LastDllError
        Exit Function
    End If
    start.cb = Len(start)
    start.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
    ' Redirect the standard output and standard error to the same pipe
    start.hStdOutput = hWritePipe
    start.hStdError = hWritePipe
    start.wShowWindow = SW_HIDE
       
    ' Start the shelled application
    ret = CreateProcessA(0&, sCmdline, sa, sa, True, NORMAL_PRIORITY_CLASS, _
                         0&, 0&, start, proc)
    If ret = 0 Then
        MsgBox "CreateProcess failed. Error: " & Err.LastDllError
        Exit Function
    End If
   
    ' The handle wWritePipe has been inherited by the shelled application
    ' so we can close it now
    CloseHandle hWritePipe

    ' Read the characters that the shelled application
    ' has outputted 256 characters at a time
    Do
        ret = ReadFile(hReadPipe, sBuffer, 256, lngBytesRead, 0&)
        sOutput = sOutput & Left$(sBuffer, lngBytesRead)
    Loop While ret <> 0 'if ret = 0 then there is no more characters to read
     
    CloseHandle proc.hProcess
    CloseHandle proc.hThread
    CloseHandle hReadPipe

    ExecuteApp = sOutput
End Function
The ExecuteApp function uses a number of API calls, I haven't give the declarations here but you can download a project that includes them at the end of this article.

The first line to note is:

    ret = CreatePipe(hReadPipe, hWritePipe, sa, 0)

This code creates a new anonymous pipe.  Later we will set it so that the shelled application will write to the pipe identified by hWritePipe and our application will read the output of the shelled application from hReadPipe.  See how a pipe is just like one in the real world, stuff goes in one end and comes out the other.

The next piece of code sets the start up properties of the shelled app.  The values specified in dwflags tells CreateProcessA to take notice of the wShowWindow and the hStdInput, hStdOutput and hStdError parameters.

    start.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW

This next bit makes it so the standard output and standard error of the shelled app are sent to the input of the pipe we created earlier.

    ' Redirect the standard output and standard error to the same pipe
    start.hStdOutput = hWritePipe
    start.hStdError = hWritePipe

And this bit actually starts application:
    ' Start the shelled application
    ret = CreateProcessA(0&, sCmdline, sa, sa, True, NORMAL_PRIORITY_CLASS, _
                         0&, 0&, start, proc)

Now we go into a loop reading up to 256 characters from the pipe at a time until there is no more characters to read.

    Do
        ret = ReadFile(hReadPipe, sBuffer, 256, lngBytesRead, 0&)
        sOutput = sOutput & Left$(sBuffer, lngBytesRead)
    Loop While ret <> 0 'if ret = 0 then there is no more characters to read


To return the output of an executed program use:
  MsgBox ExecuteApp("c:\windows\command\mem.exe) 


Pretty easy huh?

Well that's all you need to do if your using NT. To do that same thing on Windows 95 requires a little more work.

If you execute a win32 console program using ExecuteApp there should be no problem.  But if you try to execute a 16 bit dos application your application will hang.

If you step through the loop that calls ReadFile on Windows 95 it will work up to a point, and then your application will hang.

This behavior is caused by implementation differences between NT and 95.

On Windows 95 the shelled application may have closed but the redirected pipe will remain open.  When your application tries to read from the pipe that was connected to the now closed application, your application hangs.

A Solution

A solution to this is to launch a hidden Win32 console application to act as a proxy been your application and the ms-dos program. The proxy application will inherit the redirected pipes and then launch the desired ms-dos application which inherits the redirected pipes from the proxy application.

The proxy application continues to run until the ms-dos application has finished.

When the ms-dos application finishes so does the proxy application.  As the proxy application is a Win32 program it tidies up by closing the pipes when it exits.  Solving the problem with the disconnected pipe.

This proxy application implemented in C is shown below.  It takes the name of the program it should run as an argument at the command line.  The compiled version of this code is available for download here.

#include <windows.h>
#include <stdio.h>

void main (int argc, char *argv[])
{
  BOOL bRet = FALSE;
  STARTUPINFO si = {0};
  PROCESS_INFORMATION pi = {0};

  // Make child process use this app's standard files.
  si.cb = sizeof(si);
  si.dwFlags    = STARTF_USESTDHANDLES;
  si.hStdInput  = GetStdHandle (STD_INPUT_HANDLE);
  si.hStdOutput = GetStdHandle (STD_OUTPUT_HANDLE);
  si.hStdError  = GetStdHandle (STD_ERROR_HANDLE);

  bRet = CreateProcess (NULL, argv[1],
                         NULL, NULL,
                         TRUE, 0,
                         NULL, NULL,
                         &si, &pi
                         );
  if (bRet)
  {
    WaitForSingleObject (pi.hProcess, INFINITE);
    CloseHandle (pi.hProcess);
    CloseHandle (pi.hThread);
  }
}

In order for the ExecuteApp function to work on Win95 place conspawn.exe into the path and modify the CreateProcessA line to read:

   ret = CreateProcessA(0&, "conspawn """ & sCmdline & """", sa, sa, True,
      NORMAL_PRIORITY_CLASS, 0&, 0&, start, proc)

You should now be able to capture the output of a shelled 16bit program on both Windows NT and Windows 95.

Downloads

Example project including conspawn.exe

Example project without conspawn.exe

References

Q150956 - Redirection Issues on Windows 95 MS-DOS Applications
Q173085 - HOWTO: Create a Process for Reading and Writing to a Pipe