doron
doron

Reputation: 28872

Waiting on a handle in Windows Thread

I have an MFC application that launches another process using CreateProcess(...). I would like to perform a UI update when the created process terminates. Normally, I would use WaitForSingleObject or WaitForMutlipleObjecton the returned process HANDLE but this will block the GUI thread (bad).

The only solution I can think of is to spawn a new thread that can wait on the handle and post a message when the process terminates. This is not ideal.

So is it possible to register the handle with the Windows Manager and receive a Windows message when the process terminates?

Upvotes: 2

Views: 2995

Answers (5)

zett42
zett42

Reputation: 27756

You can use RegisterWaitForSingleObject() to get notified via callback, when the process has ended. The RegisterWaitForSingleObject function directs a wait thread in the thread pool to wait on the process, so this should be optimal usage of resources. As Raymond Chen commented:

The thread pool can batch multiple Wait requests into a single call to WaitForMultipleObjects so the amortized cost is 1/63 of a thread.

A minimal example of a Win32 GUI application follows. The code creates a window, then it creates another instance of itself as a child process, which is indicated by the "/child" parameter. It registers the wait callback function and runs a regular message loop. You can resize and move the window to see that the GUI is not blocked. When the child process has ended, the system asynchronously calls the wait callback which posts an application-defined message (WM_APP) to the window. When the window receives the message, it immediately calls UnregisterWait() to cancel the wait. As the reference states, even wait operations that use WT_EXECUTEONLYONCE must be canceled when the wait is completed (but not from within the callback!). Then the window shows a message box to demonstrate that it has received the message.

Error handling is omitted for brevity. You should check the return value of each API function and call GetLastError() in case FALSE is returned.

#pragma comment(linker, "/SubSystem:Windows")
#include <windows.h>
#include <string>

int APIENTRY wWinMain( HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPWSTR lpCmdLine, int /*nCmdShow*/ )
{
    if ( wcsstr( lpCmdLine, L"/child" ) )
    {
        MessageBoxW( nullptr, L"Hello from child process!", L"Child", MB_OK );
        return 0;
    }

    // Create window

    struct WindowData
    {
        HANDLE hWait = nullptr;
    }
    wndData;

    WNDCLASSW wc{};
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor( nullptr, IDC_ARROW );
    wc.hbrBackground = reinterpret_cast<HBRUSH>(GetStockObject( WHITE_BRUSH ));
    wc.lpszClassName = L"MyClass";
    wc.cbWndExtra = sizeof(LONG_PTR);
    wc.lpfnWndProc = []( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
    {
        switch ( message )
        {
        case WM_APP:
            {
                // When the wait is completed, you must call the UnregisterWait or UnregisterWaitEx function to cancel 
                // the wait operation. (Even wait operations that use WT_EXECUTEONLYONCE must be canceled.) 
                WindowData* pWndData = reinterpret_cast<WindowData*>(GetWindowLongPtr( hWnd, 0 ));
                UnregisterWait( pWndData->hWait );
                pWndData->hWait = nullptr;

                MessageBoxW( hWnd, L"Child process has ended!", L"Main", MB_OK );
            }
            break;
        case WM_DESTROY:
            PostQuitMessage( 0 );
            break;
        }
        return DefWindowProc( hWnd, message, wParam, lParam );
    };
    RegisterClassW( &wc );

    HWND hWnd = CreateWindowExW( 0, wc.lpszClassName, L"Main", WS_OVERLAPPEDWINDOW | WS_VISIBLE,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr );

    SetWindowLongPtr( hWnd, 0, reinterpret_cast<LONG_PTR>( &wndData) );

    // Create child process
    std::wstring cmd( MAX_PATH, L'\0' );
    cmd.resize( GetModuleFileNameW( nullptr, &cmd[0], cmd.size() ) );
    cmd = L"\"" + cmd + L"\" /child";
    STARTUPINFOW si{ sizeof( si ) };
    PROCESS_INFORMATION pi{};
    CreateProcessW( nullptr, &cmd[0], nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi );

    // Get notified when child process ends
    RegisterWaitForSingleObject( &wndData.hWait, pi.hProcess,
        []( PVOID lpParameter, BOOLEAN /*TimerOrWaitFired*/ )
        {
            PostMessage( reinterpret_cast<HWND>(lpParameter), WM_APP, 0, 0 );
        },
        reinterpret_cast<PVOID>(hWnd), INFINITE, WT_EXECUTEONLYONCE );

    // Run message loop
    MSG msg;
    while ( GetMessage( &msg, nullptr, 0, 0 ) )
    {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }

    // Cleanup
    if( wndData.hWait )
        UnregisterWait( wndData.hWait );
    if( pi.hProcess )
        CloseHandle( pi.hProcess );
    if( pi.hThread )
        CloseHandle( pi.hThread );

    return 0;
}

Bonus OldNewThing read: Why bother with RegisterWaitForSingleObject when you have MsgWaitForMultipleObjects?

Upvotes: 3

RonTLV
RonTLV

Reputation: 2556

The solution is creating a thread when your app is created. You then wait on an event that should be pulsed when needed. Example:

BOOL bStatus = TRUE;
CEvent mEvevnt;

// thread function
UINT LaunchThread( LPVOID p )
{
    while(bStatus && ::WaitForSingleObject(HANDLE(mEvevnt), INFINITE) == WAIT_OBJECT_0) {
            // create procees here
    }
}

// thread creation
AfxBeginThread(LaunchThread, NULL);

Trigger thread into action:

mEvevnt.PulseEvent();

You destroy the thread when your app is ending:

bStatus = FALSE;
mEvevnt.PulseEvent();

Upvotes: 0

catnip
catnip

Reputation: 25388

Good news! Windows has exactly the API you're looking for: MsgWaitForMultipleObjects ().

Tricker, is to get this into MFC's message pump, but I found this link which recommends doing the following (code untested, fixed (!), and adapted to wait on just one handle):

// virtual
BOOL CMyApp::PumpMessage()
{
    DWORD const res = ::MsgWaitForMultipleObjects
        (1, &handle_I_am_interested in, TRUE, INFINITE, QS_ALLINPUT);

    switch (res)
    {
        case WAIT_OBJECT_0 + 0:
            // the handle was signalled, strut your stuff here
            return TRUE;

        case WAIT_OBJECT_0 + 1:
            // there is a message in the queue, let MFC handle it
            return __super::PumpMessage();
    }

    // Shouldn't happen
    return TRUE;
}

I have to say that this code still doesn't look ideal to me, but it's probably close enough. I don't know enough about MFC to comment further.

Please note: This code won't see that the handle has been signalled until MFC passes through the message pump. That might happen while MessageBox() has control, for example. If that bothers you, consider using RegisterWaitForSingleObject instead, as recommended above by the legendary Raymond Chen.

Upvotes: 1

yzt
yzt

Reputation: 9103

I would do one of these two:

  1. Either call WaitForSingleObject() or whatever with a timeout of zero in your message loop somewhere (might have to change your loop to PeekMessage() or add WM_TIMER messages to make sure you check every so often,)

  2. Or better still, spawn a thread with a very small stack (you can customize that in the CreateThread() call) that only waits for this child process and then posts a message to your message loop.

I like option 2 better, since a thread with a small stack that does nothing but wait for stuff is hardly a resource drain.

Upvotes: 0

Jodocus
Jodocus

Reputation: 7581

If you can modify the code of the child process, you may just add a back-channel that will inform the parent process by SendMessage when it is about to leave. If you cannot do that, you may create a relay process that will just pass-through the original child process data (if any) but will do the information job when the child left. This is, of course, least to say far less elegent than just using a dedicated thread and e.g. WaitForSingleObject.

Upvotes: 0

Related Questions