spiegel-keyman/windows/src/engine/keyman32/serialkeyeventserver.cpp
Marc Durdin 9209adb45d refactor(windows): cleaned up hotkey change code
Just refactoring minor bits and pieces on this PR. Esp: removed the
unnecessary interrogation of TSF (yay!) and cached the debug flag so it
isn't checked on every single keystroke.
2019-11-20 06:47:44 +11:00

556 lines
18 KiB
C++

#include "pch.h"
#include "serialkeyeventserver.h"
#include "security.h"
#include "kbd.h"
#ifndef _WIN64
/*
All input is posted to the key event sender window, which then uses SendInput to post
final input to the target thread. Because SendInput calls in UWP apps will fail silently
due to restricted permissions, all SendInput must be done from this thread, which runs
in the Keyman main process.
NOTE: Postponing writing architecture technical note because of change to architecture
below...
TODO: For simplicity of proof-of-concept data sharing, we ran two copies of the key event
sender thread: one in the 32 bit space, and one in the 64 bit space. This means that we
can still have a race condition because we lose serialization guarantees. Input is first
processed in the Low Level Keyboard Hook which runs in the keyman.exe 32 bit space. This
then gets forwarded to the target application with the necessary flags on the message to
tell Keyman to not reprocess it. However, after keystroke processing, the target
application fills the shared data structure and signals the key event sender thread in its
own bitness space (32 or 64 bit). The key event sender thread then takes the final shared
data and sends it to the target app. And that breaks the serialization guarantee because
the 64 bit apps are not serialized with the original 32 bit captured input.
The fix is to redesign the shared data to use a memory mapped file, which can be shared
across the 32-64 boundary. Must tweak the permissions on this file, of course.
TODO: Console apps still not working
*/
//
// Server application functionality
// This runs only in the host applications keyman.exe and keymanx64.exe
//
// TODO: refactor this into the SerialKeyEventServer class and provide getters for them
class SerialKeyEventServer: public ISerialKeyEventServer {
private:
// Process shared data
DWORD m_idThread;
HANDLE m_hThread, m_hThreadExitEvent;
// Thread Local data
BYTE m_ModifierKeyboardState[256];
HANDLE m_hKeyEvent, m_hKeyMutex, m_hMMF;
HWND m_hwnd;
int m_nInputs;
PINPUT m_pInputs;
SerialKeyEventSharedData *m_pSharedData;
//////////////////////////////////////////////////////
// Main thread
//////////////////////////////////////////////////////
public:
SerialKeyEventServer() {
// We create the file mapping and global data on the main thread but release it on the
// local thread. This ensures that these objects are available for other processes to
// open even if we haven't completed startup of the local thread.
if (!InitSharedData()) {
return;
}
m_hThreadExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (!m_hThreadExitEvent) {
DebugLastError("CreateEvent");
return;
}
m_hThread = CreateThread(NULL, 0, ServerThreadProc, (LPVOID)this, 0, &m_idThread);
if (!m_hThread) {
DebugLastError("CreateThread");
}
}
virtual ~SerialKeyEventServer() {
if (m_hThreadExitEvent != NULL) {
if (!SetEvent(m_hThreadExitEvent)) {
DebugLastError("SetEvent");
}
if (m_hThread != NULL) {
// Wait for the thread to terminate so we know that we'll not receive
// additional events after this object is destroyed
if (WaitForSingleObject(m_hThread, 5000) != WAIT_OBJECT_0) {
DebugLastError("WaitForSingleObject(m_hThread)");
}
if (!CloseHandle(m_hThread)) {
DebugLastError("CloseHandle(m_hThread)");
}
}
if (!CloseHandle(m_hThreadExitEvent)) {
DebugLastError("CloseHandle(m_hThreadExitEvent)");
}
}
}
virtual HWND GetWindow() const {
// At destruction time, m_hwnd may be NULL
return m_hwnd;
}
private:
//////////////////////////////////////////////////////
// Global shared data management
//////////////////////////////////////////////////////
/**
This function is called by the main thread. We create the file mapping and global data on the main thread but
release it on the local thread. This ensures that these objects are available for other processes to open
even if we haven't completed startup of the local thread.
*/
BOOL InitSharedData() {
m_hMMF = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE | SEC_COMMIT, 0, sizeof(SerialKeyEventSharedData), GLOBAL_FILE_MAPPING_NAME);
if (!m_hMMF) {
DebugLastError("CreateFileMapping");
return FALSE;
}
if (!SetObjectToLowIntegrity(m_hMMF) ||
!GrantPermissionToAllApplicationPackages(m_hMMF, FILE_MAP_ALL_ACCESS)) {
return FALSE;
}
m_pSharedData = (SerialKeyEventSharedData *)MapViewOfFile(m_hMMF, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SerialKeyEventSharedData));
if (!m_pSharedData) {
DebugLastError("MapViewOfFile");
return FALSE;
}
m_hKeyMutex = CreateMutex(NULL, FALSE, GLOBAL_KEY_MUTEX_NAME);
if (!m_hKeyMutex) {
DebugLastError("CreateMutex");
return FALSE;
}
if (!SetObjectToLowIntegrity(m_hKeyMutex) ||
!GrantPermissionToAllApplicationPackages(m_hKeyMutex, MUTEX_ALL_ACCESS)) {
return FALSE;
}
m_hKeyEvent = CreateEvent(NULL, FALSE, FALSE, GLOBAL_KEY_EVENT_NAME);
if (!m_hKeyEvent) {
DebugLastError("CreateEvent");
return FALSE;
}
if (!SetObjectToLowIntegrity(m_hKeyEvent) ||
!GrantPermissionToAllApplicationPackages(m_hKeyEvent, EVENT_MODIFY_STATE)) {
return FALSE;
}
return TRUE;
}
BOOL CloseSharedData() {
BOOL bRet = TRUE;
if (m_hKeyMutex != NULL && !CloseHandle(m_hKeyMutex)) {
DebugLastError("CloseHandle(m_hKeyMutex)");
bRet = FALSE;
}
if (m_hKeyEvent != NULL && !CloseHandle(m_hKeyEvent)) {
DebugLastError("CloseHandle(m_hKeyEvent)");
bRet = FALSE;
}
if (m_pSharedData != NULL && !UnmapViewOfFile((LPCVOID)m_pSharedData)) {
DebugLastError("CloseHandle(m_pSharedData)");
bRet = FALSE;
}
if (m_hMMF != NULL && !CloseHandle(m_hMMF)) {
DebugLastError("CloseHandle(m_hMMF)");
bRet = FALSE;
}
return bRet;
}
//////////////////////////////////////////////////////
// Local thread
//////////////////////////////////////////////////////
/**
Stub callback thread procedure
*/
static DWORD WINAPI ServerThreadProc(
_In_ LPVOID lpParameter
) {
return ((SerialKeyEventServer *)lpParameter)->ThreadMain();
}
/**
Thread main procedure
*/
DWORD ThreadMain() {
if (!InitThread()) {
return 1;
}
MessageLoop();
CleanupThread();
CloseSharedData();
return 0;
}
/**
Create listener window which serializes input events and
sends out input to the target focus window, and setup local
buffers
*/
BOOL InitThread() {
m_pInputs = new INPUT[MAX_KEYEVENT_INPUTS];
GetKeyboardState(m_ModifierKeyboardState);
WNDCLASS wndClass = { 0 };
wndClass.lpfnWndProc = ServerWndProc;
wndClass.cbClsExtra = sizeof(this);
wndClass.lpszClassName = KEYEVENT_WINDOW_CLASS;
wndClass.hInstance = g_hInstance;
if (!RegisterClass(&wndClass)) {
DebugLastError("RegisterClass");
return FALSE;
}
m_hwnd = CreateWindow(KEYEVENT_WINDOW_CLASS, "", 0, 0, 0, 0, 0, HWND_MESSAGE, 0, g_hInstance, NULL);
if (m_hwnd == NULL) {
DebugLastError("CreateWindow");
return FALSE;
}
SetClassLongPtr(m_hwnd, 0, (LONG_PTR)this);
return TRUE;
}
/**
Cleanup when thread main finishes
*/
void CleanupThread() {
// Slightly naive way of locking out m_hwnd use
HWND hwnd = m_hwnd;
m_hwnd = NULL;
MemoryBarrier();
if (!DestroyWindow(hwnd)) {
DebugLastError("DestroyWindow");
}
if (!UnregisterClass(KEYEVENT_WINDOW_CLASS, g_hInstance)) {
DebugLastError("UnregisterClass");
}
if (m_pInputs != NULL) {
delete m_pInputs;
}
}
/**
Main message loop for thread. Terminates on error or when
m_hThreadExitEvent is signaled. Sleeps until either a
window message is received or a key event is signaled from
a client app.
*/
void MessageLoop() {
HANDLE events[2] = { m_hThreadExitEvent, m_hKeyEvent };
while (TRUE) {
switch (MsgWaitForMultipleObjectsEx(2, events, INFINITE, QS_ALLINPUT, 0)) {
case WAIT_OBJECT_0: // m_hThreadExitEvent signaled
// Thread has been signalled, return
return;
case WAIT_OBJECT_0 + 1: // m_hKeyEvent signaled
PostMessage(m_hwnd, WM_USER, 0, 0);
break;
case WAIT_OBJECT_0 + 2: // Windows message received
MSG msg;
while (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
DispatchMessage(&msg);
}
break;
default:
DebugLastError("MsgWaitForMultipleObjectsEx");
return;
}
}
}
/**
Reads input events from the shared buffer and sends them to the
focused window with the SendInput API.
*/
BOOL ProcessQueuedKeyEvents() {
SendDebugMessage(0, sdmDebug, 0, "Processing queued key events");
HANDLE handles[2] = { m_hThreadExitEvent, m_hKeyMutex };
//
// Wait for access to the shared data (must also watch out for
// shutdown event so we don't stall forever here)
//
switch (WaitForMultipleObjects(2, handles, FALSE, INFINITE)) {
case WAIT_OBJECT_0: // m_hThreadExitEvent signaled
// thread exit has been signalled, we are shutting down
return FALSE;
case WAIT_OBJECT_0 + 1: // m_hKeyMutex ownership granted
break;
default:
DebugLastError("WaitForMultipleObjects");
return FALSE;
}
//
// Copy the shared data from the buffer
//
PrepareInjectedInput();
//
// Reset the shared buffer and ensure the data is written out of cache for
// multiprocessor systems
//
m_pSharedData->nInputs = 0;
MemoryBarrier();
//
// Release mutex early to allow the focused application to generate more events
//
if (!ReleaseMutex(m_hKeyMutex)) {
DebugLastError("ReleaseMutex");
}
//
// Send the input to the system input queue
//
if (SendInput(m_nInputs, m_pInputs, sizeof(INPUT)) == 0) {
DebugLastError("SendInput");
}
m_nInputs = 0;
return TRUE;
}
/**
Add modifier state adjustment events and then copy the new input
events from the shared buffer
*/
void PrepareInjectedInput() {
DWORD nInputs = min(m_pSharedData->nInputs, MAX_KEYEVENT_INPUTS);
m_nInputs = 0;
keybd_shift(m_pInputs, &m_nInputs, FALSE, m_ModifierKeyboardState);
for (DWORD i = 0; i < nInputs && m_nInputs < MAX_KEYEVENT_INPUTS - MAX_KEYEVENT_INPUTS_MODIFIERS; i++, m_nInputs++) {
m_pInputs[m_nInputs].type = INPUT_KEYBOARD;
m_pInputs[m_nInputs].ki.wVk = m_pSharedData->inputs[i].wVk;
m_pInputs[m_nInputs].ki.wScan = m_pSharedData->inputs[i].wScan;
m_pInputs[m_nInputs].ki.dwFlags = m_pSharedData->inputs[i].dwFlags;
m_pInputs[m_nInputs].ki.time = m_pSharedData->inputs[i].time;
m_pInputs[m_nInputs].ki.dwExtraInfo = (ULONG_PTR)m_pSharedData->inputs[i].extraInfo;
}
keybd_shift(m_pInputs, &m_nInputs, TRUE, m_ModifierKeyboardState);
}
/**
Stub window proc that calls the g_SerialKeyEventServer wndproc
*/
static LRESULT CALLBACK ServerWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
SerialKeyEventServer *server = (SerialKeyEventServer *)GetClassLongPtr(hwnd, 0);
if (server == NULL) {
return DefWindowProc(hwnd, msg, wParam, lParam);
}
return server->WndProc(hwnd, msg, wParam, lParam);
}
/**
Process window messages for the key event sender window
*/
LRESULT WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
if (msg == WM_USER) {
ProcessQueuedKeyEvents();
}
/*
Serializes all input events back onto the focused thread by repeating any externally generated events. This
is required to ensure that we can get the correct modifier state when we need to push a modifier release/set around
keystroke output (mostly for the purposes of the backspace event).
We need to release Alt and Ctrl modifiers (and we do Shift as well for completeness) when emitting Backspace to
modify context in order to avoid triggering Alt+Backspace -> Undo or Ctrl+Backspace -> 0x7F / Word delete left
instead of a Char delete left event.
However it is possible that a modifier release event generated by the user is already in the queue at the time we
send the input. Since we have no way to detect or prevent this happening, what we do instead is re-inject each
keystroke into the queue in the focused input thread, which means we can guarantee order of events and sort out
the modifier state as required. This looks really messy in the message event queue but turns out to be robust
in practice.
You can disable this flag with flag_ShouldSerializeInput.
*/
if (msg == wm_keyman_keyevent && flag_ShouldSerializeInput /*&& _td->lpActiveKeyboard*/) {
if (wParam == VK_RMENU && (lParam & (KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP)) == (KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP) && GetKeyState(VK_LCONTROL) < 0) {
/*
When Windows has a European layout that uses AltGr installed, it can emit an additional LCtrl down via software
when RAlt is pressed. However, the corresponding LCtrl up is never received, seemingly because when Keyman
re-emits the LCtrl+RAlt, there are subtle differences in the event flags which we cannot duplicate -- specifically
the flag that emits WM_SYSKEYDOWN for the VK_LCONTROL, even though it is received before the VK_RALT event. It
appears that Windows figures this out by giving this VK_LCONTROL the scan code 0x21D instead of 0x1D. But we
are unable to emit that scan code: Windows truncates the scan code sent through SendInput so that we can only
send 0x1D.
So we simulate the release of the Left Control key ourselves when the release of the Right Alt is received,
using VK_CONTROL and scan 0x1D, and hope for the best.
The full Windows sequence is:
WM_SYSKEYDOWN VK_CONTROL 0x21D
WM_SYSKEYDOWN VK_MENU 0x38 EXTENDED_BIT
...
WM_KEYUP VK_CONTROL 0x21D
WM_KEYUP VK_MENU 0x38 EXTENDED_BIT
The best Keyman can do is:
WM_KEYDOWN VK_CONTROL 0x1D
WM_KEYDOWN VK_MENU 0x38 EXTENDED_BIT
...
WM_KEYUP VK_CONTROL 0x1D
WM_KEYUP VK_MENU 0x38 EXTENDED_BIT
There is a possibility that some apps may try and sniff that 0x21D scan code and get confused because Keyman
doesn't emit it. Hopefully this is rare.
*/
INPUT input[2];
input[0].type = INPUT_KEYBOARD;
input[0].ki.wVk = VK_CONTROL;
input[0].ki.wScan = 0x21D; // Yeah, Windows chops this to 0x1D. Such is life.
input[0].ki.time = GetMessageTime();
input[0].ki.dwExtraInfo = EXTRAINFO_FLAG_SERIALIZED_USER_KEY_EVENT;
input[0].ki.dwFlags = KEYEVENTF_KEYUP;
input[1].type = INPUT_KEYBOARD;
input[1].ki.wVk = (WORD)wParam;
input[1].ki.wScan = (lParam & 0xFFF0000) >> 16;
input[1].ki.time = GetMessageTime();
input[1].ki.dwExtraInfo = EXTRAINFO_FLAG_SERIALIZED_USER_KEY_EVENT;
input[1].ki.dwFlags = lParam & 0xFFFF;
if (!SendInput(2, input, sizeof(INPUT))) {
DebugLastError("SendInput");
}
UpdateLocalModifierState(
(BYTE)input[0].ki.wVk,
input[0].ki.dwFlags & KEYEVENTF_EXTENDEDKEY ? TRUE : FALSE,
(BYTE)input[0].ki.wScan,
input[0].ki.dwFlags & KEYEVENTF_KEYUP ? TRUE : FALSE);
UpdateLocalModifierState(
(BYTE)input[1].ki.wVk,
input[1].ki.dwFlags & KEYEVENTF_EXTENDEDKEY ? TRUE : FALSE,
(BYTE)input[1].ki.wScan,
input[1].ki.dwFlags & KEYEVENTF_KEYUP ? TRUE : FALSE);
}
else {
INPUT input;
input.type = INPUT_KEYBOARD;
input.ki.wVk = (WORD)wParam;
input.ki.wScan = (lParam & 0xFFF0000) >> 16;
input.ki.time = GetMessageTime();
input.ki.dwExtraInfo = EXTRAINFO_FLAG_SERIALIZED_USER_KEY_EVENT;
input.ki.dwFlags = lParam & 0xFFFF;
if (!SendInput(1, &input, sizeof(INPUT))) {
DebugLastError("SendInput");
}
UpdateLocalModifierState(
(BYTE)input.ki.wVk,
input.ki.dwFlags & KEYEVENTF_EXTENDEDKEY ? TRUE : FALSE,
(BYTE)input.ki.wScan,
input.ki.dwFlags & KEYEVENTF_KEYUP ? TRUE : FALSE);
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
/**
When a physical key event is received by the serializer, we know that this will
reflect the key state that the app sees at the time that the input is sent.
We maintain a local modifier state here rather than using GetKeyState because that
ensures that we are keeping the keyboard state consistent with our version of
reality.
*/
void UpdateLocalModifierState(BYTE bVk, BOOL fIsExtendedKey, BYTE bScan, BOOL fIsUp) {
switch (bVk) {
case VK_CONTROL:
// Left and right control are distinguished by a 0xE0 prefix byte
bVk = fIsExtendedKey ? VK_RCONTROL : VK_LCONTROL;
break;
case VK_MENU:
// Left and right alt are distinguished by a 0xE0 prefix byte
bVk = fIsExtendedKey ? VK_RMENU : VK_LMENU;
break;
case VK_SHIFT:
// Left and right shift are distinguished by scan code alone
bVk = bScan == SCANCODE_RSHIFT ? VK_RSHIFT : VK_LSHIFT;
break;
case VK_LCONTROL:
case VK_RCONTROL:
case VK_LSHIFT:
case VK_RSHIFT:
case VK_LMENU:
case VK_RMENU:
// These are technically not needed but perhaps some app will send them through SendInput
// and we'll have to deal with them?
break;
default:
return;
}
m_ModifierKeyboardState[bVk] = fIsUp ? 0 : 0x80;
}
};
ISerialKeyEventServer *ISerialKeyEventServer::sm_server = NULL;
void ISerialKeyEventServer::Startup() {
ISerialKeyEventServer::sm_server = new SerialKeyEventServer();
}
void ISerialKeyEventServer::Shutdown() {
delete ISerialKeyEventServer::sm_server;
}
#endif // !_WIN64