Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Low level keyboard hook set with SetWindowsHookEx stops calling function in C#

I am creating a program that monitors key presses for controlling iTunes globally. It also has a few WinForms (for displaying track information and editing options).

The low-level keyboard hook works great for awhile. If I just start up the program, keyboard hook is set and iTunes opens. Then I open Notepad and can type tons of stuff really fast and every stroke is captured, with at most 30ms being spent in the hook function (and for the most part <10ms). The hook function simply adds the events onto a queue which is processed by another thread. It is running on its own high-priority thread using it's own Application.Run().

However if I start doing things within iTunes (such as a couple of play/pause clicks which generate events in my program) or within the program (like opening the options window) then the hook function stops being called! This can happen even if the keyboard has never been used (e.g. startup, click play and pause a few times in iTunes, then press a key).

The cause of the hook not being called is not due to too much time being spent in the hook function.

When I call UnhookWindowsHookEx it always returns true, regardless if the hook function was still being called or not.

So, what could be the cause?

One idea (although I have no proof or solutions) is that the managed thread is no longer the correct native thread. I use numerous (managed) threads in my program and I have read that a single native thread can run many managed threads and that a managed thread can change which native thread is running it. Is it possible that the hook is still producing messages but sending them to the wrong thread? If this is the case, how can I work around it?


Edit: The hook and callbacks

A slightly stripped done version of my KeyMonitor. It is stripped down for clarity. I have removed some utilities (like most of the values of the Key enum and many functions of the Keys class like ToString() and FromString()) along with some error handling.

Most of the important stuff is in the KeyMonitor class. KeyMonitor.Start() starts a thread for the messages, KeyMonitor.HookThread() is that thread and creates the hook along with an Application.Run() for the message loop, KeyMonitor.KeyboardHookProc() is the callback function, and KeyMonitor.HookEventDispatchThread() is what dispatches events recorded by the callback.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace KeyTest
{
    enum Key : int
    {
        Shift = 0x10, Ctrl, Alt,
        Left_Win = 0x5B, Right_Win,
        Left_Shift = 0xA0, Right_Shift, Left_Ctrl, Right_Ctrl, Left_Alt, Right_Alt,
    }

    class Keys
    {
        [DllImport("user32.dll")]
        private static extern int GetKeyboardState(byte[] pbKeyState);
        public const int Count = 256; // vkCode are from 1 to 254, but GetKeyboardState uses 0-255

        private readonly bool[] keys = new bool[Count];

        public Keys() { }

        private void DoModifier(Key x, Key l, Key r) { keys[(int)x] = keys[(int)l] || keys[(int)r]; }
        private void DoModifiers()
        {
            DoModifier(Key.Shift, Key.Left_Shift, Key.Right_Shift);
            DoModifier(Key.Ctrl, Key.Left_Ctrl, Key.Right_Ctrl);
            DoModifier(Key.Alt, Key.Left_Alt, Key.Right_Alt);
        }
        private void DoModifier(Key x, Key l, Key r, Key k) { if (k == l || k == r) keys[(int)x] = keys[(int)l] || keys[(int)r]; }
        private void DoModifiers(Key k)
        {
            DoModifier(Key.Shift, Key.Left_Shift, Key.Right_Shift, k);
            DoModifier(Key.Ctrl, Key.Left_Ctrl, Key.Right_Ctrl, k);
            DoModifier(Key.Alt, Key.Left_Alt, Key.Right_Alt, k);
        }

        public bool this[int i] { get { return this.keys[i]; } set { this.keys[i] = value; DoModifiers((Key)i); } }
        public bool this[Key k] { get { return this.keys[(int)k]; } set { this.keys[(int)k] = value; DoModifiers(k); } }

        public void LoadCurrentState()
        {
            byte[] keyState = new byte[Count];
            if (GetKeyboardState(keyState) != 0)
                for (int i = 0; i < Count; ++i)
                    keys[i] = (keyState[i] & 0x80) != 0;
            DoModifiers();
        }
    }

    static class KeyMonitor
    {
        #region Windows API
        private delegate int HookProc(int nCode, UIntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern int UnhookWindowsHookEx(int idHook);
        [DllImport("user32.dll")]
        private static extern int CallNextHookEx(int idHook, int nCode, UIntPtr wParam, IntPtr lParam);

        private const int WH_KEYBOARD_LL = 13;
        private readonly static UIntPtr WM_KEYDOWN = new UIntPtr(0x100), WM_SYSKEYDOWN = new UIntPtr(0x104);
        #endregion

        public static event KeyEventHandler OverridingKeyChange;
        public static event KeyEventHandler KeyChange;

        private struct KeyEventData { public int vk; public bool down; }

        private static int hook = 0;
        private static Thread dispatchThread = null, hookThread = null;
        private static Keys keys = new Keys();
        private static Queue<KeyEventData> queue = new Queue<KeyEventData>();

        private static void Enqueue(int vk, bool down)
        {
            lock (queue)
            {
                queue.Enqueue(new KeyEventData() { vk = vk, down = down });
                Monitor.Pulse(queue);
            }
        }
        public static Keys Keys { get { return keys; } }

        public static void Start()
        {
            if (hook == 0)
            {
                dispatchThread = new Thread(HookEventDispatchThread);
                hookThread = new Thread(HookThread);
                hookThread.Priority = ThreadPriority.Highest;
                dispatchThread.Start();
                hookThread.Start();
            }
        }

        public static void Stop()
        {
            if (hook != 0)
            {
                // Minimal cleanup...
                UnhookWindowsHookEx(hook);
                Application.Exit();
                dispatchThread.Interrupt();
            }
        }

        private static void HookThread()
        {
            hook = SetWindowsHookEx(WH_KEYBOARD_LL, new HookProc(KeyboardHookProc), IntPtr.Zero, 0);
            if (hook == 0) { /* Handle error */ }
            keys.LoadCurrentState();
            Application.Run();
        }

        private static int KeyboardHookProc(int nCode, UIntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0)
                Enqueue(Marshal.ReadInt32(lParam), wParam == WM_SYSKEYDOWN || wParam == WM_KEYDOWN);
            return CallNextHookEx(hook, nCode, wParam, lParam);
        }

        private static void HookEventDispatchThread()
        {
            for (; ; )
            {
                KeyEventData data;
                lock (queue)
                {
                    if (queue.Count == 0)
                        try
                        {
                            Monitor.Wait(queue);
                        }
                        catch (ThreadInterruptedException) { return; }
                    data = queue.Dequeue();
                }
                if (data.vk == -1)
                {
                    // Done!
                    keys = new Keys();
                    queue.Clear();
                    return;
                }
                else if (keys[data.vk] == data.down)
                    continue;

                keys[data.vk] = data.down;
                KeyEventArgs e = new KeyEventArgs((System.Windows.Forms.Keys)data.vk);
                if (OverridingKeyChange != null) OverridingKeyChange(null, e);
                if (!e.Handled && KeyChange != null) KeyChange(null, e);
            }
        }
    }

}
like image 950
coderforlife Avatar asked Jul 22 '11 22:07

coderforlife


1 Answers

You need to save the delegate to a variable that will survive for the duration of your application. Otherwise, delegate is garbage-collected (strange the app did not crash!).

static HookProc hookProc;
...

hookProc = new HookProc(KeyboardHookProc);
hook = SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, IntPtr.Zero, 0);
like image 114
Michael Entin Avatar answered Oct 29 '22 13:10

Michael Entin