Link to home
Start Free TrialLog in
Avatar of eydelber
eydelberFlag for United States of America

asked on

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
Avatar of eydelber
eydelber
Flag of United States of America image

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
Avatar of wayside
wayside

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...
Avatar of jkr
>>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?
>>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
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
>> 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);
}


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
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
>>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);
}
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 jkr
jkr
Flag of Germany image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
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
>>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);
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
That sounds odd to me, never experienced such a thing... let me do some research.
Sorry, I honestly cannot find a reason for the behaviour you outlined - it *should* work.
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.