An Introduction to ProcessesAsynchronous Process Notification |
|
One of the questions that often comes up on the forum is the question about "How do I run another program?" This is often supplemented by a line like "I read about processes but they seem too complex". Well, sorry, you don't have a choice. You need to launch a process. This essay discusses several of the issues of process management. This is written primarily for C++/MFC programmers. This discussion supplements the discussion in our book, Win32 Programming, but you can use the information here without needing the book.
There are many answers to this, depending on what you need to accomplish. The old-fashioned C functions spawn and system still work. However, these are considered somewhat obsolete. They don't give you the control you need to receive a notification that the process completed, because you can't get the process handle.
The underlying Win32 API call to spawn a process is ::CreateProcess. This also gives you the ability to specify, for a windowing application, where on the screen the window will appear. However, ::CreateProcess is the lowest-level interface to process spawning. Microsoft recommends you use ShellExecute, which is still not good enough; while it provides the high-level interface for the best integration into the Windows environment (for example, you can give it a URL and it will launch Internet Explorer automatically if it is not running, or send the request directly to a running instance) it still doesn't provide what you need to receive a notification.
To determine if a process has stopped, you will need a process handle. This is the token Win32 uses to represent a process to an application. You can get the process handle by using either ::CreateProcess or ::ShellExecuteEx. For the best integration into Windows, Microsoft suggests (urges, demands) that you use ::ShellExecute.
Here are two examples of how to get the process handle and store it in a variable hProcess: In both cases, the functions are called with the name of the program to launch and a pointer to any arguments for its command line. If there are no arguments, the argument pointer can be NULL. The functions return a HANDLE to the process that was created, or NULL if they failed to create a process. If they return NULL, the caller can call ::GetLastError() to determine what went wrong. Note that these are "bare bones" launchers; if you want fancy control of position, startup state, console state, initial view, etc. you can work theme-and-variations on these schemes. If you want to launch a console-mode program and feed information to stdin or receive data from stdout, you will have to use ::CreateProcess, but that's the subject of another essay.
HANDLE launchViaCreateProcess(LPCTSTR program, LPCTSTR args)
{
HANDLE hProcess = NULL;
PROCESSINFO processInfo;
STARTUPINFO startupInfo;
::ZeroMemory(&startupInfo, sizeof(startupInfo));
startupInfo.cb = sizeof(startupInfo);
if(::CreateProcess(program, (LPTSTR)args,
NULL, // process security
NULL, // thread security
FALSE, // no inheritance
0, // no startup flags
NULL, // no special environment
NULL, // default startup directory
&startupInfo,
&processInfo))
{ /* success */
hProcess = processInfo.hProcess;
} /* success */
return hProcess;
}
HANDLE launchViaShellExecute(LPCTSTR program, LPCTSTR args)
{
HANDLE hProcess = NULL;
SHELLEXECUTEINFO shellInfo;
::ZeroMemory(&shellInfo, sizeof(shellInfo));
shellInfo.cbSize = sizeof(shellInfo);
shellInfo.fMask = SEE_MASK_FLAG_NOUI | SEE_MASK_NOCLOSEPROCESS;
shellInfo.lpFile = program;
shellInfo.lpParameters = args;
if(::ShellExecuteEx(&shellInfo))
{ /* success */
hProcess = shellInfo.hProcess;
} /* success */
return hProcess;
}
The nature of Windows is that a launched process takes on a life of its own. If you have a Unix background, there is nothing like the "process groups" of Unix. Once a process is launched, it has a life of its own. You have explicit control of it if you retain the process handle (and the window handle, if you get that), but if your process dies, any processes you started keep right on running. The aliveness or deadness of your process has no effect on them, unless of course they were waiting for your process to do something for them (like supply stdin text). In this case, they won't die, but they will block waiting for the desired event. So if you want to terminate a process, you have to provide a way of accomplishing this.
You might think that the way to terminate a process is to call the obvious API call, ::TerminateProcess. Wrong. Bad Move.
When you call ::TerminateProcess, the process stops. No matter what it is doing, it dies. Instantly. If it has a semaphore locked, or a mutex, or is in the middle of kernel code, or doing something else important, too bad. Boom! No more process. Imagine lots of Hollywood special effects with massive fireballs. Not a nice way to die.
A process should always have a "clean" way to be shut down. If you have hold of the handle of a process that has a window, you can send it a WM_CLOSE message via PostMessage to that window. If it is a console app, you should provide a way for it to shut down, such as detecting EOF on stdin, or receiving a particular text string. But don't use ::TerminateProcess unless you are willing to live with potentially serious consequences.
Often you will want to launch a process, often a console application, and let it run until it completes. When it completes, you can then deal with its results. For example, I have a case where I spawn (of all things) a 16-bit compiler (it is written in assembly code, and no, I had nothing to do with it; I just had to use it in a client app). I spawn it with a commandline
compilername inputfile, listingfile, outputfile
and I have to wait for it to complete before I can let the user examine the listing file or download the output file.
This is any easy one, because the compiler works with very tiny programs, and runs in under 5 seconds. So for this application, I just wait for it to complete.
HANDLE process = launcher_of_your_choice(program, args);
if(process != NULL)
{ /* success */
::WaitForSingleObject(process, INFINITE);
::CloseHandle(process);
} /* success */
However, not all programs have this property. In this case, you want to get an asynchronous notification of the completion. I do this by what appears to be a complex method, but in fact is very simple: I spawn a thread that blocks on the process handle. When the process completes, the thread resumes execution, posts a message to my main GUI window, and terminates.
I'm reproducing the code for the WaitInfo class here because it is so small. This is also part of a demo project you can download from this site.
// WaitInfo.h
class WaitInfo { public: WaitInfo() {hProcess = NULL; notifyee = NULL; } virtual ~WaitInfo() { } void requestNotification(HANDLE pr, CWnd * tell); static UINT UWM_PROCESS_TERMINATED; protected: HANDLE hProcess; // process handle CWnd * notifyee; // window to notify static UINT waiter(LPVOID p) { ((WaitInfo *)p)->waiter(); return 0; } void waiter(); };
/**************************************************************************** * UWM_PROCESS_TERMINATED * Inputs: * WPARAM: ignored * LPARAM: Process handle of process * Result: LRESULT * Logically void, 0, always * Effect: * Notifies the parent window that the process has been terminated * Notes: * It is the responsibility of the parent window to perform a * ::CloseHandle operation on the handle. Otherwise there will be * a handle leak. ****************************************************************************/
#define UWM_PROCESS_TERMINATED_MSG _T("UWM_PROCESS_TERMINATED-{F7113F80-6D03-11d3-9FDD-006067718D04}")
// WaitInfo.cpp
#include "stdafx.h"
#include "WaitInfo.h"
UINT WaitInfo::UWM_PROCESS_TERMINATED = ::RegisterWindowMessage(UWM_PROCESS_TERMINATED_MSG);
/**************************************************************************** * WaitInfo::requestNotification * Inputs: * HANDLE pr: Process handle * CWnd * wnd: Window to notify on completion * Result: void * * Effect: * Spawns a waiter thread ****************************************************************************/
void WaitInfo::requestNotification(HANDLE pr, CWnd * wnd) { hProcess = pr; notifyee = wnd;
AfxBeginThread(waiter, this); } // WaitInfo::requestNotification
/**************************************************************************** * WaitInfo::waiter * Result: void * * Effect: * Waits for the thread to complete and notifies the parent ****************************************************************************/
void WaitInfo::waiter() { ::WaitForSingleObject(hProcess, INFINITE); notifyee->PostMessage(UWM_PROCESS_TERMINATED, 0, (LPARAM)hProcess); } // WaitInfo::waiter
The way this is used is that after you have created your process, you call the requestNotification method to request a notification. You pass in the handle of the process and the window which is to receive the notification. When the process terminates, a notification message is sent to the specified window. You must have a WaitInfo object that is created before the requestNotification is called and remains valid until the notification message is received; this means that it cannot be a variable on the stack. In the example code I provide, I put it in the class header of the window class that launches the program.
In the header file for my class, I add the following:
WaitInfo requestor;
afx_msg LRESULT OnCompletion(WPARAM, LPARAM)
In the MESSAGE_MAP of the window, you need to add a line for the handler. Because this uses a qualified name, the ClassWizard is emotionally unprepared to deal with it, so you have to place it as shown, after the //}}AFX_MSG_MAP line.
//}}AFX_MSG_MAP
ON_REGISTERED_MESSAGE(WaitInfo::UWM_PROCESS_COMPLETED, OnCompletion)
END_MESSAGE_MAP()
After I launch the process, I do
HANDLE process = launcher_of_your_choice(program, args);
if(process != NULL)
{ /* success */
requestor.requestNotification(process, this);
} /* success */
The handler is quite simple:
LRESULT CMyClass::OnCompletion(WPARAM, LPARAM lParam)
{
// whatever you want to do here
::CloseHandle((HANDLE)lParam);
return 0;
}
You can study more about what I do in the sample file.
If some of the above looked confusing, you might want to read my essays on message management and worker threads.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.