Хук SetWindowsHookEx перестает работать

Перехват клавиатуры не запускает события и выдает исключение win32 при удалении

Мое приложение С# создает клавиатурный хук для обработки событий клавиатуры (многие устройства чтения карт, сканеры и другое POS-оборудование эмулируют клавиатуру). Иногда мое приложение создает клавиатурный хук без ошибок, но оно не запускает события и при удалении выдает исключение:

System.ComponentModel.Win32Exception (0x80004005): не удалось удалить перехватчики клавиатуры для «приложения». Ошибка 1404: Недопустимый дескриптор хука

Другая запись в журнале - та же ошибка, но она говорит о

ERROR_NOT_ALL_ASSIGNED

Исходный код библиотеки и демонстрационного приложения.

Я не могу воспроизвести эту проблему на своем компьютере и не знаю, что мне следует изучить или поискать в Google. Я знаю это:

  • Все клиенты с таким странным поведением используют операционную систему x86.
  • Могут быть некоторые проблемы с привилегиями или правами Windows.
  • Иногда ломается (не всегда).
  • Библиотека нацелена на .NET 4
  • Приложение нацелено на .NET 4.5.1
  • Платформа компиляции: любой процессор

Кроме того, я плохо разбираюсь в неуправляемом коде и Win API. Я получил этот код из какого-то потока и изменил его для своих нужд, но на высоком уровне абстракции.

Крючок

public GlobalKeyboardHook()
{
    _windowsHookHandle = IntPtr.Zero;
    _user32LibraryHandle = IntPtr.Zero;
    _hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.

    _user32LibraryHandle = LoadLibrary("User32");
    if (_user32LibraryHandle == IntPtr.Zero)
    {
        int errorCode = Marshal.GetLastWin32Error();
        throw new Win32Exception(errorCode,
            $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
    }


    _windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0);
    if (_windowsHookHandle == IntPtr.Zero)
    {
        int errorCode = Marshal.GetLastWin32Error();
        throw new Win32Exception(errorCode,
            $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
    }
}
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("USER32", SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);

Крючок утилизировать:

protected virtual void Dispose(bool disposing)
{
    if (disposing)
    {
        // because we can unhook only in the same thread, not in garbage collector thread
        if (_windowsHookHandle != IntPtr.Zero)
        {
            if (!UnhookWindowsHookEx(_windowsHookHandle))
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode,
                    $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
            }
            _windowsHookHandle = IntPtr.Zero;

            // ReSharper disable once DelegateSubtraction
            _hookProc -= LowLevelKeyboardProc;
        }
    }

    if (_user32LibraryHandle != IntPtr.Zero)
    {
        if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1.
        {
            int errorCode = Marshal.GetLastWin32Error();
            throw new Win32Exception(errorCode,
                $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
        }
        _user32LibraryHandle = IntPtr.Zero;
    }
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
private static extern bool FreeLibrary(IntPtr hModule);

[DllImport("USER32", SetLastError = true)]
private static extern bool UnhookWindowsHookEx(IntPtr hHook);

person Andrey Nikolaev    schedule 08.02.2018    source источник
comment
Почти уверен, что вы должны вызывать SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, IntPtr.Zero, GetCurrentThreadId ());, т.е. не передавать значение для module   -  person MickyD    schedule 09.02.2018
comment
@MickyD Нет, хуки LL являются только глобальными.   -  person Anders    schedule 09.02.2018
comment
Если бы приложение .NET установило глобальный хук в моей системе, оно было бы немедленно удалено.   -  person Anders    schedule 09.02.2018
comment
Объявления LoadLibrary() и FreeLibrary() неверны, в них отсутствует SetLastError = true. В коде нет защиты от случайного двойного вызова SetWindowsHookEx(). Это плохо кончится случайным вылетом при запуске сборщика мусора и ошибкой 1404. ERROR_NOT_ALL_ASSIGNED — это код ошибки безопасности, указывающий на то, что у учетной записи пользователя недостаточно прав. Это все, что я вижу.   -  person Hans Passant    schedule 09.02.2018
comment
@ Андерс да, конечно. Спасибо   -  person MickyD    schedule 09.02.2018
comment
@HansPassant спасибо, я добавлю это. Но если у пользователя недостаточно привилегий, моя программа вообще не должна работать, не так ли?   -  person Andrey Nikolaev    schedule 09.02.2018
comment
Я также попытаюсь решить свою проблему, используя эту библиотеку   -  person Andrey Nikolaev    schedule 09.02.2018


Ответы (1)


Если вы сделаете что-то не так в процедуре хука, Windows отключит вас, не сообщая вам об этом.

  • В вашем LowLevelKeyboardProc вы не проверяете nCode! Вы должны сделать это в первую очередь, и если nCode равно ‹ 0, вы должны вернуться с CallNextHookEx без какой-либо другой обработки.
  • Вы не можете проводить слишком много времени внутри процесса хука. Точное ограничение не задокументировано AFAIK и может быть изменено значением реестра. Вы должны стремиться к ‹ 200 мс, чтобы быть в безопасности.

Вы можете попробовать установить/изменить значение реестра LowLevelHooksTimeout в проблемных системах, чтобы посмотреть, поможет ли это.

person Anders    schedule 08.02.2018
comment
Спасибо за этот ответ! Я расследую это. Если я добавлю Thread.Sleep в обратный вызов, смогу ли я воспроизвести это на своем ПК? - person Andrey Nikolaev; 09.02.2018
comment
Я так понимаю, что LowLevelHooksTimeout существует только на системах Windows 7? Все клиенты с этой проблемой работают под управлением Windows 7. Я нашел эта статья - person Andrey Nikolaev; 09.02.2018
comment
Я предполагаю, что это Windows 7 и новее. Все системы имеют тайм-аут (вы не можете заблокировать/игнорировать ключ после тайм-аута, потому что он игнорирует возвращаемое вами значение), но только 7+ отключит вас (IIRC). - person Anders; 09.02.2018
comment
Чувак, ты спас мне жизнь. Я воспроизвел это поведение на своем компьютере после добавления Thread.Sleep(2000) в обратный вызов на моих окнах 10! - person Andrey Nikolaev; 09.02.2018
comment
Если вам на самом деле никогда не нужно есть / прерывать нажатие клавиши, вы можете реализовать процедуру ловушки как простой вызов PostMessage, который отправляет сообщение в одно из ваших окон в другом потоке. - person Anders; 10.02.2018