2

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?

Jabberwocky
  • 48,281
  • 17
  • 65
  • 115
doron
  • 27,972
  • 12
  • 65
  • 103
  • 4
    Why is spawning a new thread not ideal? – Fluffy Jun 08 '18 at 12:09
  • Threads are expensive – doron Jun 08 '18 at 12:34
  • 1
    @doron Please state your usecase where threads are too expensive. Have you measured it? Is it really the most memory-consuming part of your application? By default, it takes 1MB of RAM for its stack. You can scale this down as well. But generally, this statement is nonsense. 1 thread is not expensive, 1000 may be. – Jodocus Jun 08 '18 at 12:46
  • There is also the complexity of thread management. Ans all to work around the fact, I cannot add a handle to the windows message pump. If it has to be done, fine but it just seems a big waste. – doron Jun 08 '18 at 12:56
  • 2
    _Threads are expensive_: maybe, but lauching another process with `CreateProcess` is far more expensive, so creating a thread won't make any difference in terms of performance. – Jabberwocky Jun 08 '18 at 13:00
  • _I cannot add a handle to the windows message pump_: why would you need to do that? – Jabberwocky Jun 08 '18 at 13:02
  • 5
    Use `RegisterWaitForSingleObject`. – Raymond Chen Jun 08 '18 at 15:07
  • [This answer](https://stackoverflow.com/questions/10710912/what-is-the-process-creation-overhead-in-windows) has a comparison of thread vs. process creation overhead. If the measurement is correct, we are speaking of a ratio of 300:1 (process:thread) overhead. – zett42 Jun 08 '18 at 16:55
  • @Raymond Despite my enthusiasm for my own approach, I do have to agree that sounds a good solution. I am not well-versed in how that affects the workload of the thread pool though. Does it / can it cost a thread? Not that it probably matters in the grand scheme of things. – Paul Sanders Jun 08 '18 at 22:03
  • @PaulSanders The thread pool can batch multiple Wait requests into a single call to `WaitForMultipleObjects` so the amortized cost is 1/63 of a thread. – Raymond Chen Jun 08 '18 at 22:24
  • @Raymond, can you add RegisterWaitForSingleObject as an answer? – doron Jun 08 '18 at 23:02
  • @Raymond Sorry for my second comment. Stupid, deleted. – Paul Sanders Jun 09 '18 at 00:06

5 Answers5

2

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?

zett42
  • 25,437
  • 3
  • 35
  • 72
1

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.

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • Definitely looks interesting especially the PumpMessage part. – doron Jun 08 '18 at 14:33
  • *"Good news! Windows has exactly the API you're looking for"* - Indeed, it does. It's called [`RegisterWaitForSingleObject`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms685061.aspx). Alternatively, use a coroutine that captures the current thread context, switches to a background thread to perform the operation and wait, and then switch back to the initial thread to trigger the UI update. – IInspectable Jun 08 '18 at 15:27
  • @IInspectable *use a coroutine that captures the current thread context, switches to a background thread to perform the operation and wait, and then switch back to the initial thread to trigger the UI update* will be interesting view this in code (coroutine - this is what ? fiber ? thread context in direct sense or ? and not understand until coroutine wait - ui thread will be wait for messages ?). however i think use `MsgWaitForMultipleObjectsEx` and implement `PumpMessage` for mfc - the best solution – RbMm Jun 08 '18 at 15:54
  • Coroutines are an [experimental C++ feature](https://en.cppreference.com/w/cpp/experimental), implemented in Visual Studio. – IInspectable Jun 08 '18 at 15:57
  • @IInspectable - so some heave shell over windows thread pool api. already better `RegisterWaitForSingleObject` use in this case. but use `MsgWaitForMultipleObjectsEx` the best – RbMm Jun 08 '18 at 16:02
  • @IInspectable New one on me, that. I see plusses and minuses there. – Paul Sanders Jun 08 '18 at 16:17
  • I don't see minuses here in the same way as there are minuses with your proposed solution, which fails as soon as the application displays a modal dialog. – IInspectable Jun 08 '18 at 16:21
  • @IInspectable Minusses because it may consume a thread in the thread pool (depending on what else the thread pool is being asked to do). And you're right to say that the application won't see the 'watched' process go away while a modal dialog is open - good catch there - but that probably doesn't matter, given what the OP says he wants to do. – Paul Sanders Jun 08 '18 at 16:25
  • 1
    if modal dialog run - `CWnd::RunModalLoop` -> `AfxPumpMessage` -> `pThread->PumpMessage` - `PumpMessage` will be called even and this case. but even if thread launch some `MessageBox` which have own modal loop - so what ? after it closed - will be called main message loop and we got notify when process handle signaled. this solution not fail under any conditions – RbMm Jun 08 '18 at 16:31
  • @RbMm Oh right. Shows what I know about MFC (i.e. zilch). – Paul Sanders Jun 08 '18 at 16:33
  • @RbMm: That's true only for dialogs, that derive from MFC base classes. Surely, `MessageBox` will not do any of those. As for failure, well, at best it reports completion too late. Just because something doesn't look broken to *you*, doesn't mean that it isn't broken. Regardless of that, why don't you just skip over my contributions in the future? You always seem to know better anyway, and always seem to know, what others are looking for, so why even try to learn from me? There is nothing for you here, so don't waste time reading. – IInspectable Jun 08 '18 at 17:02
  • @IInspectable - *I would like to perform a UI update when the created process terminates.* - so will be no too late if we do this say after `MessageBox` if process terminated during messagebox running. anyway `MsgWaitForMultipleObjects` inside `PunpMessage` not lost this event and this is the best place for ui update – RbMm Jun 08 '18 at 17:16
  • @RbMm: Can you seriously not envision the bug report? *"My status bar doesn't update while I'm interacting with* *."* Would you get the same bug report when using `RegisterWaitForSingleObject`? Coroutines? Does this make you want to reconsider, what ultimately is *"best"* (as you love absolutes so much). – IInspectable Jun 08 '18 at 17:27
  • @IInspectable If there's a [non-MFC] modal dialog or message box on the screen, won't the user be looking at that, rather than the status bar? The OP is wisely keeping out of this discussion but his UI update seems to be a low-priority thing. – Paul Sanders Jun 08 '18 at 21:55
  • Don't know about you, but I find myself glancing at the status bar regardless of the UI element I'm interacting with. However, if this bothers you that much, you can change the bug report to *"My custom triggers won't fire until after closing all modal dialogs"*. Besides, you seem to be confused about what Stack Overflow is. It **isn't** a forum, where users go to, interact with others, and get *their* problems solved. SO is a Q&A site with a focus on content, not people. Even if UI updates are low-priority to this user, they may not be to a future visitor. – IInspectable Jun 09 '18 at 12:24
  • @IInspectable I agree with you about what SO is, and I have updated my answer accordingly. This conversation could have been a lot shorter, partly my fault, partly yours. – Paul Sanders Jun 09 '18 at 13:28
0

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.

Jodocus
  • 7,493
  • 1
  • 29
  • 45
  • The child process is 3rd party so not really an option, interesting idea though. – doron Jun 08 '18 at 12:32
  • @doron Then you can still create another process that you CAN control as I explained. It will work just like a thread, it will CreateProcess, it will WaitForSingleObject and then it will SendMessage to your original process. Just a relay. – Jodocus Jun 08 '18 at 12:44
  • 1
    Yes, you could do that, or any number of convoluted solutions. You *could* also just use [`RegisterWaitForSingleObject`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms685061.aspx), and call it a day. – IInspectable Jun 08 '18 at 15:35
0

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.

yzt
  • 8,873
  • 1
  • 35
  • 44
  • This is an MFC application; you do not have access to the message loop, and consequently cannot place any calls into it. Besides, polling is wrong, especially when there is a non-polling solution immediately available: `MsgWaitForMultipleObjects`. But then, that isn't applicable either, because MFC. – IInspectable Jun 08 '18 at 15:31
  • @IInspectable, I didn't know about that function, but it actually is possible to add polling to MFC. The last resort is just adding a timer (with a timeout of 1 sec or something) and poll on that message. But I agree about polling; spawning a small-stack thread is best. – yzt Jun 08 '18 at 17:43
0

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();
RonTLV
  • 2,376
  • 2
  • 24
  • 38