An MFC Process Class |
|
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.
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.
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; }
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.
Methods
Process(const CString & command, CWnd * target, UINT id = 0)
const CString & commandCommand string to execute
CWnd * targetTarget window for notification messages
UINT idProcess 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.
LRESULTLogically 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.
LRESULTLogically 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.
LRESULTLogically 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.
The most complex part of the operation is the creation of the process and establishment of the pipes.
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
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
A complete project showing the use of these classes under MFC can be downloaded. It was compiled under VC++ 6.0.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.