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;
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 };
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))
{
delete this;
return FALSE;
}
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(),
hwriteToParent,
GetCurrentProcess(),
&hwriteToParent2,
0,
TRUE,
DUPLICATE_SAME_ACCESS))
{
DWORD err = ::GetLastError();
::CloseHandle(hreadFromChild);
::CloseHandle(hwriteToParent);
::SetLastError(err);
delete this;
return FALSE;
}
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;
startup.hStdInput = NULL;
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.
::DuplicateHandle(::GetCurrentProcess(),
hreadFromChild,
::GetCurrentProcess(),
NULL,
0,
FALSE,
DUPLICATE_SAME_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.
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,
cmd,
NULL,
NULL,
TRUE,
0,
NULL,
NULL,
&startup,
&procinfo);
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)
{
DWORD err = ::GetLastError();
::CloseHandle(hreadFromChild);
::CloseHandle(hwriteToParent);
::CloseHandle(hwriteToParent2);
::SetLastError(err);
target->PostMessage(UPM_FINISHED, (WPARAM)err, (LPARAM)pid);
delete this;
return FALSE;
}
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.
::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.
CWinThread * thread = AfxBeginThread(listener, (LPVOID)this);
if(thread == NULL)
{
DWORD err = ::GetLastError();
target->PostMessage(UPM_FINISHED, (WPARAM)err, (LPARAM)pid);
delete this;
return FALSE;
}
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::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))
{
if(bytesRead == 0)
break;
buffer[bytesRead] = _T('\0');
LPTSTR b = buffer;
while(TRUE)
{
LPTSTR p = _tcschr(b, _T('\n'));
if(p == NULL)
{
*line += b;
break;
}
else
{
int offset = 0;
if(p - b > 0)
{
if(p[-1] == _T('\r'))
offset = 1;
}
*line += CString(b, (p - b) - offset);
target->PostMessage(UPM_LINE, (WPARAM)line, (LPARAM)pid);
b = p + 1;
line = new CString;
}
}
}
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.
}
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.
History
- 25th October, 2001 - Download file updated
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.