1

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:

  • The unhook function was not called
  • The program keeps capturing keystrokes, which might indicate that something is wrong with the way I process the message queue

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

  • The closer I could get on finding an answer, here in SO, was this question. However the solution did not helped me as much as I wanted.
  • The provided implementation is a minimum reproducible example and can be compiled without the need to import any external libraries.
  • A proposed solution will be to implement the capturing-keystrokes functionality as a separate child process, where will be able to start and stop whenever we like. However, I'm more interested in finding a solution using threads. I'm not sure if this is even possible (it might be).
  • The above code does not contain any error handling. This was on purpose to prevent possible over bloated of the code.

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!).

  • 1
    Well posed question. I salute you. – user4581301 Apr 09 '21 at 22:18
  • Put some debug output where you currently have a comment "// Before exit the thread, remove the installed hook." – Ben Voigt Apr 09 '21 at 22:18
  • @BenVoigt what kind of debug output would you be interested in? Just a static one or something particular. I also want to point out that the even if I'm not doing any error handling, the program runs correctly. I just committed them to prevent from over-bloated the code – George Gkasdrogkas Apr 09 '21 at 22:20
  • for what you create dedicated thread here ? – RbMm Apr 09 '21 at 23:06
  • @RbMm I want to start and end the capture functionality based on my program. This is why I though it would be best to put it in another thread. I checked your answer and the different view point you've introduced. – George Gkasdrogkas Apr 09 '21 at 23:37
  • @GeorgeGkasdrogkas - *be best to put it in another thread* i absolute sure that this is bad solution. not need any dedicated threads – RbMm Apr 09 '21 at 23:41

2 Answers2

2

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.

Ben Voigt
  • 260,885
  • 36
  • 380
  • 671
  • I was getting ready to edit my answer, when you posted the code snippet. The solution seems to work correctly. I'm about to conduct some more tests in the following hours and come back to you if anything strange happens. In any case I want to thank you very much. I'm dealing with this problem 2 days now and I'm not comfortable yet with the Windows API. I'm accepting this solution. Any comments you add and refferences are also really helpful to me. – George Gkasdrogkas Apr 09 '21 at 22:35
  • 1
    @GeorgeGkasdrogkas: I highly recommend Raymond Chen's blog "The Old New Thing". For example, this article is highly relevant: https://devblogs.microsoft.com/oldnewthing/20050405-46/?p=35973 and so is the followup: https://devblogs.microsoft.com/oldnewthing/20050406-57/?p=35963 However `MsgWaitForMulitpleObjects` should be more efficient than `WaitForSingleObject`+`WaitMessage`+`PeekMessage`. – Ben Voigt Apr 09 '21 at 22:43
  • `PeekMessage` exist sense call only when `MsgWaitForMultipleObjects` return `WAIT_OBJECT_0 + nCount` - so `WAIT_OBJECT_0 + 1` in concrete case – RbMm Apr 09 '21 at 23:05
  • @RmMm: Yes, I wrote the conditional as "loop while it isn't the stop event" but you could equally well express it as "loop while it is the message-waiting status" – Ben Voigt Apr 12 '21 at 14:38
1

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,

RbMm
  • 25,803
  • 2
  • 21
  • 40
  • Thank you for your answer @RbMm. It's true that in real world scenarios there exist one message loop for the entire app in most case. I posted the question the way it is to be able provide a minimum reproducible example. – George Gkasdrogkas Apr 09 '21 at 23:34
  • @GeorgeGkasdrogkas - yes, and in real world (i assume that you call `SetWindowsHookExW` based on some event form UI) must be single common message loop. based on *MsgWaitForMultipleObjectsEx* . not need create separate threads - this is 100%. and separate message loops – RbMm Apr 09 '21 at 23:39
  • 1
    @GeorgeGkasdrogkas - and many solutions is possible. you can for example create timer, after call *SetWindowsHookExW* and on *WM_TIMER* call *UnhookWindowsHookEx* and cancel timer.. many way exist really – RbMm Apr 09 '21 at 23:49