Click here to Skip to main content
Click here to Skip to main content

An MFC Process Class

, 25 Oct 2001
Rate this:
Please Sign up or sign in to vote.
This class allows you to create a child process and receive notification of its output.

Child Processes with output

One of the common problems that arises is how to fire off a child process and collect its output. This class allows you to create a child process and receive notification of its output.

The technique includes the method of having a worker thread post messages to the main GUI thread, as described in my essay on worker threads.

Using the Class

The class is invoked quite simply. The class is designed so you can actually have several classes running concurrently, and sort the output from the various child classes if you need to.

Simple Usage

void CMyView::OnRun()
{
 CString cmd;  // the command to execute
 cmd = ...;
 Process * p = new Process(cmd, this);
 if(!p->run())
    ... failed
}

This creates a Process object to execute the command string passed in. Notifications of events will be posted to the window passed in as the second parameter. Note that this must not be a NULL pointer. If any error occurs, or when the process completes, the Process object will be automatically deleted. The run method actually calls the CreateProcess API. Control returns immediately upon creating the process; it does not wait for process completion.

There are two events which you will need to handle in the CWnd class that receives notifications:

UPM_LINE is sent for each input line that is received, passing a CString * containing the line contents to the target window.

UPM_FINISHED notifies the window that the thread has finished, which allows the window to re-enable controls, menus, etc.

These are Registered Window Messages. The IMPLEMENT_MSG macro is used to declare them in the module in which they are used:

IMPLEMENT_MSG(UPM_LINE)
IMPLEMENT_MSG(UPM_FINISHED)

You must declare handlers for these in your header file:

afx_msg LRESULT OnLine(WPARAM, LPARAM)
afx_msg LRESULT OnFinished(WPARAM, LPARAM)

and install the entries in the MESSAGE_TABLE:

ON_REGISTERED_MESSAGE(UPM_LINE, OnLine)
ON_REGISTERED_MESSAGE(UPM_FINISHED, OnFinished)

A typical handler is to use a CListBox as the logging control. The example uses a simple CListBox with the Sorted option disabled (your output is pretty useless most of the time if it is simply sorted alphabetically), or you may use something more elaborate such as my Logging ListBox Control.

LRESULT CMyClass::OnLine(WPARAM wParam, LPARAM)
{
 CString * s = (CString *)wParam;
 c_Output.AddString(*s);
 delete s;
 return 0;
}

Multiple Process Usage

If you want to use multiple processes concurrently, you need to distinguish the events. The way this is handled is that you create a unique UINT to represent a process. This ID value will be sent with every message, and you have to use it to determine which of your child processes generated the message. Note that this is an id you assign; it is not a process ID or process handle. How you sort out the results is up to you. As long as you have a unique ID for the child process, it can be dynamically generated or a simple constant.

Reference

Methods

Process(const CString & command, CWnd * target, UINT id = 0)

const CString & command

Command string to execute

CWnd * target

Target window for notification messages

UINT id

Process identifier (application-generated), default is zero.

Creates a Process object and initializes it to the specified parameters. This does not create a system process, only a process object. A Process object must always be allocated from the heap, because it will be automatically destroyed when the process terminates.

BOOL run()

Creates a process and the thread to receive data from it. Control returns immediately. If the process and thread were created successfully, returns TRUE, else returns FALSE. If the value FALSE is returned, the Process object is immediately destroyed. If TRUE is returned, the Process object exists and will exist until the process terminates. Note that there are no methods that can be called after the process is created that will have any meaning, and consequently there is no reason to retain the Process object pointer once the run method has been invoked.

Messages

UPM_PROCESS_HANDLE
WPARAM	(WPARAM)(HANDLE)

The process handle associated with the child process

LPARAM	(LPARAM)(UINT)

The id value established by the Process constructor.

LRESULT

Logically void, 0, always.

UPM_LINE
WPARAM	(WPARAM)(CString *)

A string object representing one line of output captured from the child process. The CR and LF have been stripped from the string. The recipient of this message is responsible for deleting this CString object when its value is no longer needed.

LPARAM	(LPARAM)(UINT)

The id value established by the Process constructor.

LRESULT

Logically void, 0, always.

UPM_FINISHED
WPARAM	(WPARAM)(DWORD)

The error code from ::GetLastError representing the reason for failure, or 0 if there was no error.

LPARAM	(LPARAM)(UINT)

The id value established by the Process constructor.

LRESULT

Logically void, 0, always.

This message is sent under two conditions: The worker thread failed to create, and thus there will be no further output delivered, or the ReadFile from the child process either returned an EOF condition or has terminated with an ERROR_BROKEN_PIPE error.

How it works

The most complex part of the operation is the creation of the process and establishment of the pipes. 

Process::run

BOOL Process::run()
    {
     hreadFromChild = NULL;

hreadFromChild is a member variable of the Process class, and is used by the worker thread to read from the child process. The other two handles, below, have no need to exist beyond this function.

     HANDLE hwriteToParent = NULL;
     HANDLE hwriteToParent2 = NULL;

     SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
     // inheritable handle

The SECURITY_ATTRIBUTES is used to create inheritable handles; the last member of the structure is set to TRUE so that the handle will be inheritable.

     if(!::CreatePipe(&hreadFromChild, &hwriteToParent, &sa, 0))
	{ /* pipe failed */
	 // ::GetLastError() will reveal the cause
	 delete this;
	 return FALSE;
	} /* pipe failed */

The CreatePipe operation creates a single unidirectional anonymous pipe, and returns handles to its read and write ends. They are, by the SECURITY_ATTRIBUTES, inheritable handles.

     if(!::DuplicateHandle(GetCurrentProcess(),     // duplicate from this process
			   hwriteToParent,	    // this handle 
			   GetCurrentProcess(),     // into this process
			   &hwriteToParent2,        // as this handle
			   0,			    // no access flags 
                                  // (subsumed by DUPLICATE_SAME_ACCESS)
			   TRUE,	            // create inheritable
			   DUPLICATE_SAME_ACCESS))  // create duplicate access
	{ /* duplicate failed */
	 DWORD err = ::GetLastError();
	 ::CloseHandle(hreadFromChild);
	 ::CloseHandle(hwriteToParent);
	 ::SetLastError(err);
	 delete this;
	 return FALSE;
	} /* duplicate failed */

The handle which is to be passed down for standard output will also be passed in for the standard error handler. A number of child processes, including the command interpreter, have a tendency to close the standard error handle if they are not going to use it. If we pass in the same handle for both output and error, when the child process closes the error handle, it necessarily closes the output handle. This means you could never see the output from such a process. By using DuplicateHandle, we get a duplicate handle representing the same stream. If the child process closes this duplicate handle (which we pass in as the error handle), the output handle remains active.

     STARTUPINFO startup;
     PROCESS_INFORMATION procinfo;

     ::ZeroMemory(&startup, sizeof(startup));

     startup.cb = sizeof(startup);
     startup.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
     startup.wShowWindow = SW_HIDE; // hidden console window
     startup.hStdInput = NULL; // not used
     startup.hStdOutput = hwriteToParent;
     startup.hStdError = hwriteToParent2;

Here we initialize the structure. Note that in keeping with good programming practice, the structure should first be zeroed. The standard input handle is not set. If you have a child process that requires input, you would want to set it here, using a variant of this code. You would also have to create a thread to provide the data for the input stream.

     // We want a non-inherited read handle. DuplicateHandle with a
     // NULL target fixes the read side to be non-inheritable
     ::DuplicateHandle(::GetCurrentProcess(),    // in this process
		       hreadFromChild,           // child read handle
		       ::GetCurrentProcess(),    // to this process
		       NULL,                     // modify existing handle
		       0,                        // flags
		       FALSE,                    // not inheritable
		       DUPLICATE_SAME_ACCESS);   // same handle access

This seems a little odd; we are creating a "duplicate" without specifying a target (the fourth parameter is NULL). This is an odd idiom that is used. We want to inherit the output handle so the child process can send output, but we don't want to inherit the input side of the handle. One of the effects of DuplicateHandle is that if the target handle address is given as NULL, it modifies the input handle. By setting the inheritable characteristic to FALSE, the handle is rendered non-inheritable.

     // We need a writeable buffer for the command (silly Windows restriction)
     LPTSTR cmd = command.GetBuffer(command.GetLength() + 1);

The CreateProcess call requires an LPTSTR, not an LPCTSTR (constant string). Therefore, we cannot use the CString value of command as a parameter. In particular, the buffer must be modifiable. We use GetBuffer to get a modifiable buffer, and we allow space for a character than might be appended to the command line (read the CreateProcess documentation). 

     BOOL started = ::CreateProcess(NULL,        // command is part of input string
				    cmd,         // (writeable) command string
				    NULL,        // process security
				    NULL,        // thread security
				    TRUE,        // inherit handles flag
				    0,           // flags
				    NULL,        // inherit environment
				    NULL,        // inherit directory
				    &startup,    // STARTUPINFO
				    &procinfo);  // PROCESS_INFORMATION

The CreateProcess call is very straightforward. I have done nothing here to allow for any of the thread or process security options, modifications of the environment or directory, etc. If you need such features, you can enhance the Process constructor to supply such values and create member variables to hold them. The only difference between this CreateProcess call and other instances of the call is that the fifth parameter is TRUE, meaning all inheritable handles will be inherited by the child process. This is how we pass the standard handles to the child.

     command.ReleaseBuffer();

     if(!started)
	{ /* failed to start */
	 DWORD err = ::GetLastError(); // preserve across CloseHandle calls
	 ::CloseHandle(hreadFromChild);
	 ::CloseHandle(hwriteToParent);
	 ::CloseHandle(hwriteToParent2);
	 ::SetLastError(err);
	 target->PostMessage(UPM_FINISHED, (WPARAM)err, (LPARAM)pid);
	 delete this;
	 return FALSE;
	} /* failed to start */

Note that if the process fails to start, the target receives a UPM_FINISHED message, although it will not have received a UPM_PROCESS_HANDLE message. Note that the WPARAM is the error code of why the process creation failed.

     target->PostMessage(UPM_PROCESS_HANDLE, (WPARAM)procinfo.hProcess, (LPARAM)pid);

The PostMessage call notifies the target window that the process has started, and passes the process handle in. I don't know what good the process handle will do, but it seemed a reasonable thing to pass in. Note that this is passed in only if the process has started successfully.

     // Now close the output pipes so we get true EOF/broken pipe
     ::CloseHandle(hwriteToParent);
     ::CloseHandle(hwriteToParent2);

If the child process terminates, the handle is closed, but what is really closed is the handle in the child process. The hwriteToParent and hwriteToParent2 handles remain valid. Consequently, the ReadFile operation would not receive a broken pipe error, and would hang forever, waiting for some other process which might have an active handle to send it data. By closing our own copies of the handles, this means that the only handles available are those of the child process, and when it terminates, they will be implicitly closed. Since this will mean the last instance of the handle has been closed, the pipe will be broken and ReadFile will receive the correct notification.

     // We have to create a listener thread. We create a worker
     // thread that handles this
     CWinThread * thread = AfxBeginThread(listener, (LPVOID)this);
     if(thread == NULL)
	{ /* failed */
         DWORD err = ::GetLastError();
	 target->PostMessage(UPM_FINISHED, (WPARAM)err, (LPARAM)pid);
	 delete this;
	 return FALSE;
	} /* failed */

This creates a worker thread to receive the data from the child process. Note that if this cannot be created, no output will arrive at the controlling thread window. Therefore, I send a UPM_FINISHED message to notify the window that the process is effectively terminated. Note that the error code describing the failure mode is passed back.

     return TRUE;
    } // Process::run

Process::listener

This is the second-level method (see my technique for thread creation); this is the non-static method which is executed in the context of the thread and the Process object. This reads the input stream, which may contain several lines of text, splits the line up, and sends each line to the parent. 

#define MAX_LINE_LENGTH 1024

void Process::listener()
    {
     TCHAR buffer[MAX_LINE_LENGTH + 1];

     CString * line;
     line = new CString;

     DWORD bytesRead;
     
     while(::ReadFile(hreadFromChild, buffer, dim(buffer) - 1, &bytesRead, NULL))
	{ /* got data */
	 if(bytesRead == 0)
	    break; // EOF condition
	 
	 buffer[bytesRead] = _T('\0');
	 // Convert to lines
	 LPTSTR b = buffer;
	 while(TRUE)
	    { /* convert and send */
	     LPTSTR p = _tcschr(b, _T('\n'));
	     if(p == NULL)
		{ /* incomplete line */
		 *line += b;
		 break; // leave assembly loop
		} /* incomplete line */
	     else
		{ /* complete line */
		 int offset = 0;
		 if(p - b > 0)
		    { /* get rid of \r */
		     if(p[-1] == _T('\r'))
			offset = 1;
		    } /* get rid of \r */
		 *line += CString(b, (p - b) - offset);
		 target->PostMessage(UPM_LINE, (WPARAM)line, (LPARAM)pid);
		 b = p + 1;
		 line = new CString;
		} /* complete line */
	    } /* convert and send */
	} /* got data */

     DWORD err = ::GetLastError();

     ::CloseHandle(hreadFromChild);

We are now done reading, so we close the handle to clean it up. Otherwise, we would have a lot of handles left around when the program finished.

     if(line->GetLength() > 0)
	target->PostMessage(UPM_LINE, (WPARAM)line, (LPARAM)pid);
     else
	delete line;

The above lines send any partially-built line that remains to the target window. However, if no content is in the string, the CString object still needs to be deleted.

     DWORD status = 0;
     if(err != ERROR_BROKEN_PIPE)
	status = err;

A normal EOF or an ERROR_BROKEN_PIPE are valid termination conditions. If it is any other error, something went wrong, so the status is passed back via the UPM_FINISHED message.

     target->PostMessage(UPM_FINISHED, status, (LPARAM)pid);

     delete this;

This final operation deletes the Process object. Since otherwise there is no good way to track this, I chose to do it here. This means that only a Process * variable can be used to hold a Process object; a Process variable cannot be declared.

    } // Process::listener

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

History

25 Oct 2001 - download file updated.

Send mail to newcomer@flounder.com with questions or comments about this article.
Copyright © 1999 All Rights Reserved
http://www.flounder.com/mvp_tips.htm

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Joseph M. Newcomer

United States United States
No Biography provided

Comments and Discussions

 
QuestionDeadlock? Pinmembermasanli26-Nov-07 17:18 
AnswerRe: Deadlock? PinmemberJoseph M. Newcomer27-Nov-07 5:58 
GeneralRe: Deadlock? Pinmembermasanli27-Nov-07 16:34 
GeneralRe: Deadlock? PinmemberJoseph M. Newcomer27-Nov-07 19:10 
GeneralRe: Deadlock? Pinmembermasanli28-Nov-07 18:22 
I checked your sample code. A duplicated handle was used for stderr. There is no error happened in the child thread. Task Manager shows that the child thread takes CPU. It seems that the pipe is blocked for some reason and the thread hangs due to the blocking.
 
The DOS program runs fine from outside. There are intensive disk data transfer in the DOS program. Do you think there is a possibility of interference bewteen the DOS program file read/write and the anonymous pipe?
 
I did try the other methods that I found online, but problem is the same.
 
I'll greatly appreciate if you can think of any reason. Thanks.
GeneralRe: Deadlock? PinmemberJoseph M. Newcomer28-Nov-07 19:12 
GeneralYour code will not work always PinmemberElmue15-Dec-06 23:56 
GeneralDoesn't seem to work with xcopy Pinmemberpluggy9-Aug-06 10:26 
GeneralShowWindow deadlock Pinmemberanna_mouse19-Jun-06 6:07 
GeneralObservations PinmemberPeter Ritchie26-Apr-05 8:38 
GeneralRe: Observations PinmemberJoseph M. Newcomer1-May-05 17:08 
Generalfailing threads PinmemberHenk van den Toorn15-Dec-04 3:08 
GeneralRe: failing threads PinmemberJoseph M. Newcomer18-Dec-04 15:16 
GeneralRe: failing threads Pinmemberhenk van den toorn27-Dec-04 1:03 
GeneralRe: failing threads PinmemberJoseph M. Newcomer27-Dec-04 4:42 
GeneralHiding the console Pinmemberronpeters4-Dec-03 6:38 
GeneralEnter a deadlock and run out the Memory! PinmemberHighersong31-Oct-03 21:02 
GeneralRe: Enter a deadlock and run out the Memory! PinmemberHighersong31-Oct-03 21:17 
GeneralRe: Enter a deadlock and run out the Memory! PinmemberJoseph M. Newcomer31-Oct-03 21:20 
GeneralPassing instruction to the screen Pinmembervgandhi13-Aug-03 13:40 
GeneralRe: Passing instruction to the screen PinmemberJoseph M. Newcomer14-Aug-03 4:39 
GeneralRe: Passing instruction to the screen Pinmembervgandhi14-Aug-03 4:59 
GeneralRe: Passing instruction to the screen PinmemberJoseph M. Newcomer14-Aug-03 5:52 
GeneralDelayed Output Problem Pinmembergbg1088-Jan-03 8:55 
GeneralRe: Delayed Output Problem PinmemberJoseph M. Newcomer8-Jan-03 11:48 
GeneralRe: Delayed Output Problem Pinmembergbg10817-Jan-03 6:42 
GeneralRe: Delayed Output Problem PinmemberRJSoft23-Aug-04 2:23 
GeneralRe: Delayed Output Problem PinmemberJoseph M. Newcomer23-Aug-04 5:40 
GeneralRe: Delayed Output Problem PinmemberAju.George24-May-05 3:46 
GeneralRe: Delayed Output Problem PinmemberJoseph M. Newcomer28-May-05 5:03 
GeneralRe: Delayed Output Problem PinmemberAju.George2-Jun-05 22:09 
GeneralOutput intensive process Pinmemberphyzics30-Dec-02 5:33 
GeneralRe: Output intensive process PinmemberJoseph M. Newcomer30-Dec-02 8:53 
GeneralRe: Output intensive process Pinmemberphyzics30-Dec-02 13:52 
QuestionHow write to the process PinmemberAnders M Eriksson11-Oct-02 12:03 
AnswerRe: How write to the process PinmemberJoseph M. Newcomer11-Oct-02 13:05 
QuestionWould you like to develop a .NET component version of Process? PinmemberSman Zheng22-May-02 23:45 
GeneralIt Work at win2k but fail when run at win9* and me PinmemberLUOXU22-Mar-02 18:16 
GeneralRe: It Work at win2k but fail when run at win9* and me PinmemberJoseph M. Newcomer22-Mar-02 18:29 
QuestionHow to send commands PinmemberAnonymous15-Mar-02 0:03 
AnswerRe: How to send commands PinmemberAnonymous4-Jul-02 22:14 
GeneralRe: How to send commands PinmemberJoseph M. Newcomer5-Jul-02 4:19 
Generaltags not done right PinmemberNish [BusterBoy]26-Oct-01 4:46 
GeneralWorkspace and Project files not included PinmemberAnonymous17-Oct-01 18:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 26 Oct 2001
Article Copyright 2001 by Joseph M. Newcomer
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid