Link to home
Create AccountLog in
C++

C++

--

Questions

--

Followers

Top Experts

Avatar of eydelber
eydelber🇺🇸

Windows keyboard hook as Windows Service
Hello,

I just bought an IBM Thinkpad T-42, and found a couple of useless "Web Forward" and "Web Backwards" that I wrote a program to remap to Music Forwards and Music Backwards, something I use much more.  The program sets a keyboard hook with SetWindowsHookEx.  The standalone program set the hook, then popped up a message box, during which everything worked fine.  Once you clicked on the message box, the procedure would unhook and then the program finished.  Great for debugging and testing, now to make something useful...

So I went ahead and modified the program to be a Windows Service.  The actual keyboard hook is in a DLL.  Everything service-wise is working fine (i.e. Starting, Stopping, etc.), but the hook no longer works!  I did not change the code of how the hook was set (into a DLL), so I'm confused why this part no longer works.  The reason I know it doesn't work, is that I have some logging code, and I added a log to the top of the Keyboard Hook function that traps the keys to just say that the function was called... and it never gets called.

Any ideas?

Here's some code (parts taken out that I thought were irrelevant).

First, the DLL with the hook procedure:

[code]
BYTE vk_next = VK_MEDIA_NEXT_TRACK,
       vk_prev = VK_MEDIA_PREV_TRACK;

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
      Log("DllMain: called");
      return TRUE;
}

...

LRESULT CALLBACK LowLevelKeyboardProc(int nCode,WPARAM wParam,LPARAM lParam)
{
      Log("LowLevelKeyboardProc: Called");
      BOOL bSuppress = FALSE;

...

      return (bSuppress ? TRUE : CallNextHookEx(NULL, nCode, wParam, lParam));
}
[/code]

And then the service:

[code]
#define DLL_LOCATION "ibmremaplib.dll"
#define SERVICE_NAME "IBM Keyboard Remapper"

HOOKPROC hkprcSysMsg;
static HINSTANCE hinstDLL;
static HHOOK hhookSysMsg;
SERVICE_STATUS          ServiceStatus;
SERVICE_STATUS_HANDLE   hStatus;

HHOOK Start();
BOOL Stop();
void GetErrorString(LPVOID lpMsgBuf);
void ServiceMain(int argc, char** argv);
void ControlHandler(DWORD request);

HHOOK Start()
{
      hhookSysMsg = SetWindowsHookEx(WH_KEYBOARD_LL, hkprcSysMsg, hinstDLL, 0);
      return hhookSysMsg;
}

BOOL Stop()
{
      return UnhookWindowsHookEx(hhookSysMsg);
}

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE prevInstance, PSTR pszCmdLine, int iCmdShow)
{
      LPVOID lpMsgBuf = NULL;
      hinstDLL = LoadLibrary((LPCTSTR) DLL_LOCATION);
      if(!hinstDLL)
      {
            GetErrorString(&lpMsgBuf);
            //MessageBox(NULL, (LPTSTR)lpMsgBuf, TEXT("Error1"), 0);
            Log((PTSTR) lpMsgBuf);
            return S_FALSE;
      }
      hkprcSysMsg = (HOOKPROC)GetProcAddress(hinstDLL, "LowLevelKeyboardProc");
      if(!hkprcSysMsg)
      {
            GetErrorString(&lpMsgBuf);
            Log((PTSTR)lpMsgBuf);
            return S_FALSE;
      }
      Log("WinMain: called");
      SERVICE_TABLE_ENTRY ServiceTable[2];
      ServiceTable[0].lpServiceName = SERVICE_NAME;
      ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

      ServiceTable[1].lpServiceName = NULL;
      ServiceTable[1].lpServiceProc = NULL;
      // Start the control dispatcher thread for our service
      StartServiceCtrlDispatcher(ServiceTable);
      LocalFree(lpMsgBuf);
      return S_OK;
}

void GetErrorString(void *lpMsgBuf)
{
      FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
            NULL,
            GetLastError(),
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
            (LPTSTR) lpMsgBuf,
            0,
            NULL
      );
}

void ServiceMain(int argc, char** argv)
{
      ServiceStatus.dwServiceType = SERVICE_WIN32;
      ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
      ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
      ServiceStatus.dwWin32ExitCode = 0;
      ServiceStatus.dwServiceSpecificExitCode = 0;
      ServiceStatus.dwCheckPoint = 0;
      ServiceStatus.dwWaitHint = 0;

      hStatus = RegisterServiceCtrlHandler(
            SERVICE_NAME,
            (LPHANDLER_FUNCTION)ControlHandler);
      if(hStatus == (SERVICE_STATUS_HANDLE)0)
      {
            // Registering Control Handler failed
            Log(TEXT("Registering Control Handler failed."));
            return;
      }

      // We report the running status to SCM.
      ServiceStatus.dwCurrentState = SERVICE_RUNNING;
      SetServiceStatus (hStatus, &ServiceStatus);

      if(!Start())
      {
            Log(TEXT("ServiceMain: Could not hook!"));
      }
      else
      {
            TCHAR msg[50];
            Log(TEXT("ServiceMain: Started"));
            sprintf(msg, "%0.8X", hhookSysMsg);
            Log(msg);
      }
      while(ServiceStatus.dwCurrentState == SERVICE_RUNNING)
      {
            Sleep(5000);
      }
}

void ControlHandler(DWORD request)
{
      switch(request)
      {
      case SERVICE_CONTROL_STOP:
      case SERVICE_CONTROL_SHUTDOWN:
            if(!Stop())
            {
                  Log(TEXT("ControlHandler: Could not unhook!"));
            }
            else Log(TEXT("ControlHandler: Stopped."));
            ServiceStatus.dwWin32ExitCode = 0;
            ServiceStatus.dwCurrentState = SERVICE_STOPPED;
            SetServiceStatus (hStatus, &ServiceStatus);
            return;
      }

      // Report current status
      SetServiceStatus (hStatus, &ServiceStatus);
}

[/code]

Thanks for any help!
Kevin Grigorenko

Zero AI Policy

We believe in human intelligence. Our moderation policy strictly prohibits the use of LLM content in our Q&A threads.


Avatar of eydelbereydelber🇺🇸

ASKER

Also, I put in debug code and made sure that LoadLibrary, GetProcAddress, and SetWindowsHookEx did not have any problems, so the DLL is hooked in, I have the address of LowLevelKeyboardProc, and SetWindowHookEx returns a proper HHOOK.  I also used Spy++ and the service does have two threads going.

Kevin

First thing I would do is in the Start() function, if SetWindowsHookEx() returns a 0, I would call GetLastError() or your GetErrorString() function, to see what the error is, and log it.

In fact, you should check the error code for every Windows API call. The error might not be in the Start() function.

Hmmm, sounds like you did that already...

Reward 1Reward 2Reward 3Reward 4Reward 5Reward 6

EARN REWARDS FOR ASKING, ANSWERING, AND MORE.

Earn free swag for participating on the platform.


Avatar of jkrjkr🇩🇪

>>Everything service-wise is working fine (i.e. Starting, Stopping, etc.), but the hook no longer works!

Thats the exact problem. Hooks execute in *user* contexts, not in services' context and they do not even share the same window station. I'd rather recommend to start these hooks like 'Autorun' apps, which ensures the proper context. BTW, what's your program supposed to do exactly?

Avatar of eydelbereydelber🇺🇸

ASKER

>>Thats the exact problem. Hooks execute in *user* contexts, not in services' context and they do not even share the same window station. I'd rather recommend to start these hooks like 'Autorun' apps, which ensures the proper context. BTW, what's your program supposed to do exactly?

Ahh, interesting.  I wouldn't mind just running it in autorun, but how do I just let it sit there without a while(1) loop, which obviously doesn't work.  Do I need to use a semaphore or something?  I really just need to give my hook to windows and that's it.  My program doesn't need to do anything else.

Here's the actual code, like I mentioned it maps a few keys on my thinkpad keyboard to different keys (in this case multimedia playlist forward and backward keys):

      BOOL bSuppress = FALSE;

      if(nCode == HC_ACTION) {
            switch (wParam) {
            case WM_KEYDOWN:
            case WM_SYSKEYDOWN:
            case WM_KEYUP:
            case WM_SYSKEYUP:
                  PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT) lParam;
                  bSuppress = NeedsRemap(p->vkCode);
                  if(bSuppress)
                  {
                        DWORD dwFlags = 0;
                        if(wParam == WM_KEYUP ||
                              wParam == WM_SYSKEYUP)
                        {
                              dwFlags |= KEYEVENTF_KEYUP;
                        }
                        // inject the appropriate logic:

                        // below is specific logic for the backward/forward
                        // buttons
                        if(p->scanCode != 0) /* quirk; see preamble */
                        {
                              if(p->vkCode == VK_IBM_MEDIA_PREV_TRACK)
                              {
                                    // backwards key
                                    keybd_event(vk_prev, 0, dwFlags, 0);
                              }
                              else if(p->vkCode == VK_IBM_MEDIA_NEXT_TRACK)
                              {
                                    // forwards key
                                    keybd_event(vk_next, 0, dwFlags, 0);
                              }
                        }
                  }
                  break;
            }
      }

Thanks for the help,
Kevin

Avatar of eydelbereydelber🇺🇸

ASKER

Actually, jkr, could you explain your answer a little bit more just for my knowledge?  What if I run the service as Local System, or am I missing your point?  Also, I thought the DLL was just sucked in (for lack of a better term) to the executable and would work just as when it was working in standalone?

Kevin

Free T-shirt

Get a FREE t-shirt when you ask your first question.

We believe in human intelligence. Our moderation policy strictly prohibits the use of LLM content in our Q&A threads.


Avatar of jkrjkr🇩🇪

>> I wouldn't mind just running it in autorun, but how do I just let it sit there without a while(1) loop,
>>which obviously doesn't work

That would ideed be a bad idea. All you need is a windowless app that installs the hook and the just waits for an event to terminate, e.g.

int PASCAL WinMain      (      HANDLE      hInstance,
                                    HANDLE      hPrevInst,
                                    LPSTR      lpszCmdLine,
                                    int            cmdShow
                              )
{
HANDLE            hev;

      if      (      !(      hev      =      CreateEvent      (      NULL,      
                                                            FALSE,      
                                                            FALSE,      
                                                            MYAPP_EVENT_NAME
                                                      )
                  )
            )
            {
                  if      (      ERROR_ALREADY_EXISTS      ==      GetLastError      ())
                        {
                              if      (      !(      hev      =      OpenEvent      (      EVENT_ALL_ACCESS,      
                                                                                    FALSE,      
                                                                                    MYAPP_EVENT_NAME
                                                                              )
                                          )
                                    )
                                    {
                                          return      (      -1);
                                    }
                        }
                   else      
                        {
                              return      (      -2);
                        }
            }

      InstallHook();


      WaitForSingleObject      (      hev,      INFINITE);

      CloseHandle      (      hev);
}



Avatar of eydelbereydelber🇺🇸

ASKER

That's exactly what I was looking for!!!  I'll try to get it working tonight.

And InstallHook() needs to be on its own thread, correct?  Because I assume WaitForSingleObject is synchronous, and even if my hook call is in a separate DLL, the only thread of execution will be waiting, right?

Thanks,
Kevin

Avatar of eydelbereydelber🇺🇸

ASKER

Okay, as per my last comment, I can't get it to work.  If I try my code as before, be installing the hook, and popping up a message box, it works.  But then if I take that out and let it run through to WaitForSingleObject, it no longer works (perhaps based on the hypothesis I mentioned in the previous post).  I tried creating a new thread and put the WaitForSingleObject call there, but then main ended, and I guess it took that thread with it.  Then I tried to put the Install Hook in a new thread, but that didn't work either.  It seems to be a problem because the hook is installed as a callback but the main thread is synchronized and waiting?

Thanks,
Kevin

Reward 1Reward 2Reward 3Reward 4Reward 5Reward 6

EARN REWARDS FOR ASKING, ANSWERING, AND MORE.

Earn free swag for participating on the platform.


Avatar of jkrjkr🇩🇪

>>But then if I take that out and let it run through to WaitForSingleObject, it no longer works (perhaps based on the
>>hypothesis I mentioned in the previous post).

Err, no - that function is supposed to reside in the hook DLL. Here's a sample taken out of one of my previous projects:

LONG __DYNLINK InstallHook  ( void)
{
    HANDLE  hev;

    if  (   g_hhk)  return  (   ERROR_ALREADY_EXISTS);

    g_hhk   =   SetWindowsHookEx    (   WH_GETMESSAGE,
                                        ( HOOKPROC) HookProc,
                                        g_hThisDll,
                                        0
                                    );

    if  (   !(  hev =   CreateEvent (   NULL,  
                                        FALSE,  
                                        FALSE,  
                                        MYAPP_EVENT_NAME
                                    )
            )
        )
        {
            if  (   ERROR_ALREADY_EXISTS    ==  GetLastError    ())
                {
                    if  (   !(  hev =   OpenEvent   (   EVENT_ALL_ACCESS,  
                                                        FALSE,  
                                                        MYAPP_EVENT_NAME
                                                    )
                            )
                        )
                        {
                            return  (   -1);
                        }
                }
             else  
                {
                    return  (   -2);
                }
        }

    WaitForSingleObject (   hev,    INFINITE);

    Sleep   (   5000);

    return  (   0);
}

LONG __DYNLINK TerminateHook    ( void)
{
    HANDLE  hev;

    UnhookWindowsHookEx (   g_hhk);

    Sleep   (   5000);

    if  (   !(  hev =   OpenEvent   (   EVENT_ALL_ACCESS,  
                                        FALSE,  
                                        MYAPP_EVENT_NAME
                                    )
            )
        )
        {  
            return  (   GetLastError    ());
        }

    PulseEvent  (   hev);

    return  (   0);
}

Avatar of eydelbereydelber🇺🇸

ASKER

I'm probably doing something wrong.  I tried your code, but it's still not working.  Here is all of the code.

First, the DLL:

>> The DLL header, "ibmremaplib.h":

#ifndef IBMREMAPLIB_H
#define IBMREMAPLIB_H

// A few #define's because these are only supported for
// Win2k and above
#ifndef VK_MEDIA_NEXT_TRACK
#define VK_MEDIA_NEXT_TRACK 0xB0
#endif

#ifndef VK_MEDIA_PREV_TRACK
#define VK_MEDIA_PREV_TRACK 0xB1
#endif

// The virtual key codes found on my T-42.
#define VK_IBM_MEDIA_PREV_TRACK 0xA6
#define VK_IBM_MEDIA_NEXT_TRACK 0xA7

#define MYAPP_EVENT_NAME "IBMKeyboardRemapper"

LONG InstallHook();
LONG TerminateHook();

#endif

>> The DLL source def file:

LIBRARY ibmremaplib
EXPORTS
LowLevelKeyboardProc
InstallHook
TerminateHook

>> The DLL source file, "ibmremaplib.cpp"

#define WIN32_LEAN_AND_MEAN            // Exclude rarely-used stuff from Windows headers
#define _WIN32_WINNT 0x0400
#include <windows.h>
#include "ibmremaplib.h"

BYTE vk_next = VK_MEDIA_NEXT_TRACK,
       vk_prev = VK_MEDIA_PREV_TRACK;

static HHOOK g_hhk = NULL;
static HINSTANCE g_hThisDll;

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
      g_hThisDll = hInstance;
      return TRUE;
}

/*
 * Utility function to see if we will be dealing
 * with this key.
 */
BOOL NeedsRemap(DWORD key)
{
      if(key == VK_IBM_MEDIA_PREV_TRACK ||
            key == VK_IBM_MEDIA_NEXT_TRACK)
      {
            return TRUE;
      }
      return FALSE;
}

/*
 * The procedure we hook into Windows.
 */
LRESULT CALLBACK LowLevelKeyboardProc(
      int nCode,
      WPARAM wParam,
      LPARAM lParam
)
{
      // If this flag is set, the current key
      // will be suppressed. (Except in the case
      // that processing takes longer than the timeout
      // values defined in HKEY_CURRENT_USER\Control Panel\Desktop)
      BOOL bSuppress = FALSE;

      if(nCode == HC_ACTION) {
            switch (wParam) {
            case WM_KEYDOWN:
            case WM_SYSKEYDOWN:
            case WM_KEYUP:
            case WM_SYSKEYUP:
                  PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT) lParam;
                  bSuppress = NeedsRemap(p->vkCode);
                  if(bSuppress)
                  {
                        DWORD dwFlags = 0;
                        if(wParam == WM_KEYUP ||
                              wParam == WM_SYSKEYUP)
                        {
                              dwFlags |= KEYEVENTF_KEYUP;
                        }
                        // inject the appropriate logic:

                        // below is specific logic for the backward/forward
                        // buttons
                        if(p->scanCode != 0) /* quirk; see preamble */
                        {
                              if(p->vkCode == VK_IBM_MEDIA_PREV_TRACK)
                              {
                                    // backwards key
                                    keybd_event(vk_prev, 0, dwFlags, 0);
                              }
                              else if(p->vkCode == VK_IBM_MEDIA_NEXT_TRACK)
                              {
                                    // forwards key
                                    keybd_event(vk_next, 0, dwFlags, 0);
                              }
                        }
                  }
                  break;
            }
      }
      return (bSuppress ? TRUE : CallNextHookEx(NULL, nCode, wParam, lParam));
}

LONG InstallHook()
{
      HANDLE hev;
      if(g_hhk) return(ERROR_ALREADY_EXISTS);

      g_hhk = SetWindowsHookEx(
            WH_KEYBOARD_LL,
            (HOOKPROC) LowLevelKeyboardProc,
            g_hThisDll,
            0
      );

      if(!(hev = CreateEvent(NULL, FALSE,      FALSE, MYAPP_EVENT_NAME)))
      {
            if(GetLastError() == ERROR_ALREADY_EXISTS)
            {
                  if(!(hev = OpenEvent(EVENT_ALL_ACCESS, FALSE, MYAPP_EVENT_NAME)))
                  {
                        return(-1);
                  }
            }
            else
            {
                  return(-2);
            }
      }

      WaitForSingleObject(hev, INFINITE);

      Sleep(5000);

      return(0);
}

LONG TerminateHook()
{
      HANDLE hev;

      UnhookWindowsHookEx(g_hhk);

      Sleep(5000);

      if(!(hev = OpenEvent(EVENT_ALL_ACCESS, FALSE, MYAPP_EVENT_NAME)))
      {
            return(GetLastError());
      }

      PulseEvent(hev);

      return(0);
}

And finally, the driver program, "ibmremap.cpp":

#define WIN32_LEAN_AND_MEAN            // Exclude rarely-used stuff from Windows headers
#define _WIN32_WINNT 0x0400
#include <windows.h>
#include <process.h>
#include "ibmremaplib.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR pszCmdLine, int iCmdShow)
{
    HANDLE hev;
    if(!(hev = CreateEvent(NULL,
                           FALSE,
                           FALSE,
                           MYAPP_EVENT_NAME
                          )))
    {
        if(GetLastError() == ERROR_ALREADY_EXISTS)
        {
            if(!(hev = OpenEvent(EVENT_ALL_ACCESS,
                                 FALSE,
                                 MYAPP_EVENT_NAME
                                )))
            {
                return(-1);
            }
        }
        else
        {
            return(-2);
        }
    }
      InstallHook();
      WaitForSingleObject(hev, INFINITE);
      TerminateHook();
                CloseHandle(hev);
      return S_OK;
}


Thanks,
Kevin

ASKER CERTIFIED SOLUTION
Avatar of jkrjkr🇩🇪

Link to home
membership
Log in or create a free account to see answer.
Signing up is free and takes 30 seconds. No credit card required.
Create Account

Avatar of eydelbereydelber🇺🇸

ASKER

Hey, great, thanks; it worked.... kind of.  I first tried it by copying your code verbatim, only changing the arguments to SetWindowsHookEx to WH_KEYBOARD_LL and my proc; however, it didn't work (it slowed down my keyboard inputs tremendously).  So, I was about to come back and say that something was wrong.  Before I did that, I tried putting your WH_GETMESSAGE back in and adding a proc that handles that type of hook - and it worked!  However, this means that I can't suppress the key (the low level proc is needed).  I am able now to catch it and do what I want, but the key still goes through.  I would like to use WH_KEYBOARD_LL, but clearly there is something specific with that that is causing problems (although I know it works standalone).  And I basically ported the code from to the WH_GETMESSAGE, so the code is the same.  Do you have any ideas why the low level keyboard proc does not act like the getmessage proc?

Thanks so much!  I'm halfway there.
Kevin Grigorenko

Free T-shirt

Get a FREE t-shirt when you ask your first question.

We believe in human intelligence. Our moderation policy strictly prohibits the use of LLM content in our Q&A threads.


Avatar of jkrjkr🇩🇪

>>However, this means that I can't suppress the key (the low level proc is needed)

Actually, you can remove a message in a WH_GETMESSAGE hook by using

MSG* pMsg = (MSG*) lParam;
MSG msg;
PeekMessage(&msg,pMsg->hWnd,0,0,PM_REMOVE);

Avatar of eydelbereydelber🇺🇸

ASKER

Two problems:

1. The PeekMessage remove doesn't work.  Here is how my hook ends:

      if(bSuppress)
      {
            MSG tempMsg;
            PeekMessage(&tempMsg,msg->hwnd,0,0,PM_REMOVE);
            return 0;
      }
      else
            return CallNextHookEx(NULL, nCode, wParam, lParam);

The actual key injection is working, so bSuppress is true, but the original key still goes through.

2. A more perplexing problem.  The code works for the first 5 minutes or so, and then without any intervention stops working (the program is still running in the background, and debug messages are printed to a file, but the hook seems to somehow have been unhooked).  When I restart, it works again, but again for only 5 minutes or so.  Why is my hook getting unhooked?  I have even left the computer as is for 5 minutes and it did the samet thing, so it is not under my intervention.

Thanks so much,
Kevin

Avatar of jkrjkr🇩🇪

That sounds odd to me, never experienced such a thing... let me do some research.

Reward 1Reward 2Reward 3Reward 4Reward 5Reward 6

EARN REWARDS FOR ASKING, ANSWERING, AND MORE.

Earn free swag for participating on the platform.


Avatar of jkrjkr🇩🇪

Sorry, I honestly cannot find a reason for the behaviour you outlined - it *should* work.

Avatar of eydelbereydelber🇺🇸

ASKER

It's okay, close enough, thanks so much for all of the help!

Kevin

Garbage collector most likely disposed your hook pointer. Put GC.KeepAlive(HookPointer) somewhere at the end of your code so the Garbage collector knows not to dispose of that hook.

Free T-shirt

Get a FREE t-shirt when you ask your first question.

We believe in human intelligence. Our moderation policy strictly prohibits the use of LLM content in our Q&A threads.

C++

C++

--

Questions

--

Followers

Top Experts

C++ is an intermediate-level general-purpose programming language, not to be confused with C or C#. It was developed as a set of extensions to the C programming language to improve type-safety and add support for automatic resource management, object-orientation, generic programming, and exception handling, among other features.