mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-17 13:17:39 +00:00
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.
556 lines
18 KiB
C++
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
|