Code for this article: Jun98Win32Code.exe (6KB)
Jeffrey Richter wrote Advanced Windows, Third Edition (Microsoft Press, 1998) and Windows 95: A Developer's Guide (M&T Books, 1995). Jeff is a consultant and teaches Win32 programming courses (www.solsem.com). He can be reached at www.JeffreyRichter.com.|
Q My application, MainApp, spawns another process, WorkerApp, to do some additional work. When WorkerApp is running, it may spawn even more child processes that do even more work. While MainApp is waiting for WorkerApp and its children to complete, the user may want to stop this additional work from continuing. To do this, I have MainApp kill WorkerApp, but this is not sufficientWorkerApp's child processes must terminate as well.
In other words, I'm trying to emulate the behavior of Microsoft® Developer Studio®. When I build a project, Developer Studio runs NMAKE and that, in turn, spawns additional child processes like CL.EXE (the compiler). CL.EXE spawns C2.EXE (the second pass of the compiler), and so on. But if I select the Stop Build option from the Developer Studio Build menu, all of these processes are terminated. How do I accomplish this for my own applications?
A I'm quite surprised that I haven't been asked this question before. This seems like a common thing to want to do, but Win32® doesn't offer an easy way to accomplish it. The Win32 specification states that no relationship exists between a parent process and any of its child processes once a child process has been created. Many other operating systems support a parent/child relationship policy, so if you kill a process, the system automatically kills all of the process's descendant processes.
Since Win32 doesn't support this policy, there must be some other way to solve the problem. Initially, I thought I would need to walk a process parent/child relationship list somehow, get the list of processes spawned from the parent process that I want to kill, and then recursively kill all of its descendants. Since Win32 doesn't support this policy, there are no Win32 functions that return this information.
But wait, perhaps I spoke too soon. Windows® 95 offers a set of ToolHelp functions that are not formally part of the Win32 API specification. In particular, the Process32First and Process32Next functions both initialize the members of a PROCESSENTRY32 structure:
DWORD th32ParentProcessID; // Look Here!
th32ParentProcessID is the ID of the process that created the process I'm looking at. Now I can walk the set of processes in the system and determine which are descendants of the main process that I want to kill. I can then call OpenProcess, TerminateProcess, and CloseHandle for each of these descendant processes. I think you'll agree that this is not such a horrible solution.
However, Windows NT® 4.0 does not support the ToolHelp functions. Since these functions are not part of the Win32 specification, you would not be able to get your application to work correctly using this method on Windows NT 4.0. On the other hand, the ToolHelp functions have been so useful, Microsoft has seen fit to include them in Windows NT 5.0.
If the ToolHelp functions don't work on Windows NT 4.0 or earlier, what can you do on Windows NT today? Well, the TLIST.EXE tool in the Windows NT Resource Kit makes calls to undocumented functions to produce a tree showing the parent/child relationships between processes (note that this tool doesn't run on Windows 95). Figure 1 shows an example of TLIST's output.
You can see that Explorer.exe (with process ID 140) is
the parent of CMD.EXE (process ID 59), which is the parent of TLIST.EXE (process ID 196). When your application runs on Windows NT, you could spawn TLIST.EXE and redirect its output to a file or memory buffer. Then you
could parse the contents of this buffer to get the descendant process information and kill all the child processes. Of course, this requires your customers to have TLIST.EXE installed because Microsoft doesn't allow you to redistribute this utility.
There is another problem with both of the methods shown: there is no guarantee that the process is still running when you get the process IDs back. One of the child processes could terminate and a completely different process that has nothing to do with your application could start and be assigned a process ID that was previously used by your child process. This error would cause your application to terminate some other process, making your users very upset. You could get around this problem by writing more complicated code, but a better solution that works on both Windows 95 and Windows NT would be great.
Every console process runs in a process group. A process group is a set of processes that receives a notification when special events occur such as the user pressing Ctrl+C or Ctrl+Break, the user closing a console window, a system logoff, or a system shutdown event. If you have several console processes running in a single process group, then all of the processes in the group receive this notification. To solve your process-control problem, all you have to do is make sure that all of the processes you are spawning are console applications running in a single process group. Then you can use the GenerateConsoleCtrlEvent function to force a Ctrl+Break notification to all of these processes:
DWORD dwCtrlEvent, // pass CTRL_BREAK_EVENT here
DWORD dwProcessGroupId // pass 0 here
By default, when a console process receives a Ctrl+Break notification, the process terminates itself. What could
Now let's put all of this information together. First, in your MainApp application, spawn the new process by calling CreateProcess, passing the DETACHED_PROCESS, CREATE_NEW_CONSOLE, or CREATE_NEW_PROCESS_
GROUP flag. All of these flags cause the system to create a new process group that contains only the newly spawned process. Any processes spawned from this new process will be part of the same process group. (If a grandchild process is a GUI application or a console application spawned using the DETACHED_PROCESS, CREATE_NEW_CONSOLE, or CREATE_NEW_PROCESS_GROUP flag, then my solution fails.)
The process ID of the first process in a process group is used to notify all of the processes within a single process group. You will need to pass this process ID as the second parameter to the GenerateConsoleCtrlEvent function to identify which process group will receive the event notification. You would think that MainApp just has to call GenerateConsoleCtrlEvent when you want to terminate all the processes in the process group.
Unfortunately, it's not quite that simple. You see, GenerateConsoleCtrlEvent has a small feature that complicates this issue a little more. If you read the documentation for GenerateConsoleCtrlEvent closely, you'll see the following statement: "The GenerateConsoleCtrlEvent function sends a specified signal to a console process group that shares the console associated with the calling process." This additional requirement means the Main-App program will not be able to call GenerateConsoleCtrlEvent successfully because it is running in a different process group.
To solve this problem you must write another small application, SmallApp. MainApp will spawn SmallApp in its own process group, and SmallApp will be responsible for spawning any additional processes. You will need to set up some other type of communication mechanism between MainApp and SmallApp. When MainApp wants to terminate the process group, it will use a well-defined IPC mechanism (like an event kernel object) that tells SmallApp to kill its own process group.
Figures 2 and 3 show all of the steps necessary to make MainApp and SmallApp do the right thing. The code is commented, so I won't go into any additional details about it here. By the way, Developer Studio ships with a small application called VCSPAWN.EXE that exists for exactly this purpose.
By the way, Microsoft provided a much better way of solving this problem with Window NT 5.0. The solution comes courtesy of the all-new Job kernel object. A Job object allows multiple processes to be grouped together and treated as a single entity. So if you were to rewrite MainApp using Job objects, you would do the following:
Now, MainApp determines when the Job is done by waiting on the Job object, or MainApp can forcibly kill all processes in the Job object by calling TerminateJobObject. Note that you can kill all processes associated with a Job object regardless of whether these applications are console or GUI applications.
- Delete the SmallApp application entirely.
- Call CreateJobObject before spawning the killable process.
- Call CreateProcess to spawn the killable process. (Create this process suspended.)
- Call AssignProcessToJobObject to associate the new process with your Job object. Note that if the new process spawns any additional processes, the new processes are automatically assigned to this same job. That's why you must spawn this new process suspended. If you didn't, the new process could spawn a child process before MainApp had a chance to associate the first process with the Job.
- Call ResumeThread to resume the spawned process's primary thread.
There are several more cool things that you can do with Job objects on Windows NT 5.0. I strongly encourage you to examine the documentation and prepare for the future.
Have a question about programming in Win32?
Send your questions via email to Jeffrey Richter from his website at http://www.jeffreyrichter.com.