georgegkas
georgegkas

Reputation: 427

Cannot exit message loop from thread using Windows API and C++

I'm trying to implement the following scenario:

Requirement

Write a C++ program to capture all the keyboard inputs on Windows OS. The program should start capturing keystrokes and after about 3 seconds (the specific amount time is not very relevant, it could be 4/5/etc.), the program should stop capturing keystrokes and continue its execution.

Before I proceed with the actual implementation details, I want to clarify that I preferred tο write the requirements in a form of exercise, rather than providing a long description. I'm not trying to gather solutions for homework. (I'm actually very supportive to such questions when its done properly, but this is not the case here).

My solution

After working on different implementations the past few days, the following is the most complete one yet:

#include <iostream>
#include <chrono>
#include <windows.h>
#include <thread>

// Event, used to signal our thread to stop executing.
HANDLE ghStopEvent;

HHOOK keyboardHook;

DWORD StaticThreadStart(void *)
{
  // Install low-level keyboard hook
  keyboardHook = SetWindowsHookEx(
      // monitor for keyboard input events about to be posted in a thread input queue.
      WH_KEYBOARD_LL,

      // Callback function.
      [](int nCode, WPARAM wparam, LPARAM lparam) -> LRESULT {
        KBDLLHOOKSTRUCT *kbs = (KBDLLHOOKSTRUCT *)lparam;

        if (wparam == WM_KEYDOWN || wparam == WM_SYSKEYDOWN)
        {
          // -- PRINT 2 --
          // print a message every time a key is pressed.
          std::cout << "key was pressed " << std::endl;
        }
        else if (wparam == WM_DESTROY)
        {
          // return from message queue???
          PostQuitMessage(0);
        }

        // Passes the keystrokes
        // hook information to the next hook procedure in the current hook chain.
        // That way we do not consume the input and prevent other threads from accessing it.
        return CallNextHookEx(keyboardHook, nCode, wparam, lparam);
      },

      // install as global hook
      GetModuleHandle(NULL), 0);

  MSG msg;
  // While thread was not signaled to temirnate...
  while (WaitForSingleObject(ghStopEvent, 1) == WAIT_TIMEOUT)
  {
    // Retrieve the current messaged from message queue.
    GetMessage(&msg, NULL, 0, 0);
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  // Before exit the thread, remove the installed hook.
  UnhookWindowsHookEx(keyboardHook);

  // -- PRINT 3 --
  std::cout << "thread is about to exit" << std::endl;

  return 0;
}

int main(void)
{
  // Create a signal event, used to terminate the thread responsible
  // for captuting keyboard inputs.
  ghStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

  DWORD ThreadID;
  HANDLE hThreadArray[1];

  // -- PRINT 1 --
  std::cout << "start capturing keystrokes" << std::endl;

  // Create a thread to capture keystrokes.
  hThreadArray[0] = CreateThread(
      NULL,              // default security attributes
      0,                 // use default stack size
      StaticThreadStart, // thread function name
      NULL,              // argument to thread function
      0,                 // use default creation flags
      &ThreadID);        // returns the thread identifier

  // Stop main thread for 3 seconds.
  std::this_thread::sleep_for(std::chrono::milliseconds(3000));

  // -- PRINT 4 --
  std::cout << "signal thread to terminate gracefully" << std::endl;

  // Stop gathering keystrokes after 3 seconds.
  SetEvent(ghStopEvent);

  // -- PRINT 5 --
  std::cout << "from this point onwards, we should not capture any keystrokes" << std::endl;

  // Waits until one or all of the specified objects are
  // in the signaled state or the time-out interval elapses.
  WaitForMultipleObjects(1, hThreadArray, TRUE, INFINITE);

  // Closes the open objects handle.
  CloseHandle(hThreadArray[0]);
  CloseHandle(ghStopEvent);

  // ---
  // DO OTHER CALCULATIONS
  // ---

  // -- PRINT 6 --
  std::cout << "exit main thread" << std::endl;

  return 0;
}

Implementation details

The main requirement is the capturing of keystrokes for a certain amount of time. After that time, we should NOT exit the main program. What I thought would be suitable in this case, is to create a separate thread that will be responsible for the capturing procedure and using a event to signal the thread. I've used windows threads, rather than c++0x threads, to be more close to the target platform.

The main function starts by creating the event, followed by the creation of the thread responsible for capturing keystrokes. To fulfill the requirement of time, the laziest implementation I could think of was to stop the main thread for a certain amount of time and then signaling the secondary one to exit. After that we clean up the handlers and continue with any desired calculations.

In the secondary thread, we start by creating a low-level global keyboard hook. The callback is a lambda function, which is responsible for capturing the actual keystrokes. We also want to call CallNextHookEx so that we can promote the message to the next hook on the chain and do not disrupt any other program from running correctly. After the initialization of the hook, we consume any global message using the GetMessage function provided by the Windows API. This repeats until our signal is emitted to stop the thread. Before exiting the thread, we unhook the callback.

We also output certain debug messages throughout the execution of the program.

Expected behavior

Running the above code, should output similar messages like the ones bellow:

start capturing keystrokes
key was pressed 
key was pressed 
key was pressed 
key was pressed 
signal thread to terminate gracefully
thread is about to exit
from this point onwards, we should not capture any keystrokes
exit main thread

Your output might differ depending on the number of keystrokes that were captured.

Actual behavior

This is the output I'm getting:

start capturing keystrokes
key was pressed 
key was pressed 
key was pressed 
key was pressed 
signal thread to terminate gracefully
from this point onwards, we should not capture any keystrokes
key was pressed 
key was pressed
key was pressed

A first glance into the output reveals that:

There is something wrong regarding the way I'm reading the messages from the message queue, but after hours of different approaches, I could not find any solution for the specific implementation. It might also be something wrong with the way I'm handling the terminate signal.

Notes

For any questions you might have, feel free to comment! Thank you in advance for your time to read this question and possibly post an answer (it will be amazing!).

Upvotes: 1

Views: 659

Answers (2)

RbMm
RbMm

Reputation: 33706

What I thought would be suitable in this case, is to create a separate thread that will be responsible for the capturing procedure

it's not necessary to do this if another thread will just wait for this thread and nothing to do all this time

you can use code like this.

LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    if (HC_ACTION == code)
    {
        PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;

        DbgPrint("%x %x %x %x\n", wParam, p->scanCode, p->vkCode, p->flags);
    }

    return CallNextHookEx(0, code, wParam, lParam);
}

void DoCapture(DWORD dwMilliseconds)
{
    if (HHOOK hhk = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, 0, 0))
    {
        ULONG time, endTime = GetTickCount() + dwMilliseconds;

        while ((time = GetTickCount()) < endTime)
        {
            MSG msg;
            switch (MsgWaitForMultipleObjectsEx(0, 0, endTime - time, QS_ALLINPUT, MWMO_INPUTAVAILABLE))
            {
            case WAIT_OBJECT_0:
                while (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
                {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
                break;

            case WAIT_FAILED:
                __debugbreak();
                goto __0;
                break;

            case WAIT_TIMEOUT:
                DbgPrint("WAIT_TIMEOUT\n");
                goto __0;
                break;
            }
        }
__0:
        UnhookWindowsHookEx(hhk);
    }
}

also in real code - usual not need write separate DoCapture with separate message loop. if your program before and after this anyway run message loop - posiible all this do in common message loop,

Upvotes: 1

Ben Voigt
Ben Voigt

Reputation: 283624

I think this is your problem:

  while (WaitForSingleObject(ghStopEvent, 1) == WAIT_TIMEOUT)
  {
    // Retrieve the current messaged from message queue.
    GetMessage(&msg, NULL, 0, 0);
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

The reason is that currently your loop can get stuck on the GetMessage() step forever and never again look at the manual-reset event.

The fix is simply to replace the combination of WaitForSingleObject + GetMessage with MsgWaitForMultipleObjects + PeekMessage.

The reason you've made this mistake is that you didn't know GetMessage only returns posted messages to the message loop. If it finds a sent message, it calls the handler from inside GetMessage, and continues looking for posted message. Since you haven't created any windows that can receive messages, and you aren't calling PostThreadMessage1, GetMessage never returns.

while (MsgWaitForMultipleObjects(1, &ghStopEvent, FALSE, INFINITE, QS_ALLINPUT) > WAIT_OBJECT_0) {
   // check if there's a posted message
   // sent messages will be processed internally by PeekMessage and return false
   if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
}

1 You've got logic to post WM_QUIT but it is conditioned on receiving WM_DESTROY in a low-level keyboard hook, and WM_DESTROY is not a keyboard message. Some hook types could see a WM_DESTROY but WH_KEYBOARD_LL can't.

Upvotes: 2

Related Questions