Here is a technique that, although not infallible, can be useful if, for some reason, you cannot use the task object. The idea is to create an anonymous pipe and allow the child process to inherit the handle to the end of the pipe.
Typically, grandchild processes also inherit the end of a pipe record. In particular, processes started with cmd.exe (for example, from a batch file) inherit descriptors.
After the child process exits, the parent process closes its handle to the end of the recording in the pipe and then tries to read from this channel. Since no one writes to the pipe, the read operation will block indefinitely. (Of course, you can use threads or asynchronous I / O if you want to keep doing things while waiting for grandchildren.)
When (and only when) the last descriptor at the end of the pipe record is closed, the end of the record will be automatically destroyed. This breaks the channel and the read operation completes and reports an ERROR_BROKEN_PIPE error.
I have been using this code (and earlier versions of the same code) in production for several years.
// pwatch.c // // Written in 2011 by Harry Johnston, University of Waikato, New Zealand. // This code has been placed in the public domain. It may be freely // used, modified, and distributed. However it is provided with no // warranty, either express or implied. // // Launches a process with an inherited pipe handle, // and doesn't exit until (a) the process has exited // and (b) all instances of the pipe handle have been closed. // // This effectively waits for any child processes to exit, // PROVIDED the child processes were created with handle // inheritance enabled. This is usually but not always // true. // // In particular if you launch a command shell (cmd.exe) // any commands launched from that command shell will be // waited on. #include <windows.h> #include <stdio.h> void error(const wchar_t * message, DWORD err) { wchar_t msg[512]; swprintf_s(msg, sizeof(msg)/sizeof(*msg), message, err); printf("pwatch: %ws\n", msg); MessageBox(NULL, msg, L"Error in pwatch utility", MB_OK | MB_ICONEXCLAMATION | MB_SYSTEMMODAL); ExitProcess(err); } int main(int argc, char ** argv) { LPWSTR lpCmdLine = GetCommandLine(); wchar_t ch; DWORD dw, returncode; HANDLE piperead, pipewrite; STARTUPINFO si; PROCESS_INFORMATION pi; SECURITY_ATTRIBUTES sa; char buffer[1]; while (ch = *(lpCmdLine++)) { if (ch == '"') while (ch = *(lpCmdLine++)) if (ch == '"') break; if (ch == ' ') break; } while (*lpCmdLine == ' ') lpCmdLine++; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; if (!CreatePipe(&piperead, &pipewrite, &sa, 1)) error(L"Unable to create pipes: %u", GetLastError()); GetStartupInfo(&si); if (!CreateProcess(NULL, lpCmdLine, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) error(L"Error %u creating process.", GetLastError()); if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_FAILED) error(L"Error %u waiting for process.", GetLastError()); if (!GetExitCodeProcess(pi.hProcess, &returncode)) error(L"Error %u getting exit code.", GetLastError()); CloseHandle(pipewrite); if (ReadFile(piperead, buffer, 1, &dw, NULL)) { error(L"Unexpected data received from pipe; bug in application being watched?", ERROR_INVALID_HANDLE); } dw = GetLastError(); if (dw != ERROR_BROKEN_PIPE) error(L"Unexpected error %u reading from pipe.", dw); return returncode; }