From 711541be60301d79ede3ae7c2bce8a67f3aa94cd Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 17 Nov 2025 16:30:43 +0100 Subject: [PATCH] fix(windows): reinstall low level keyboard hook if it gets removed This change improves the stability of Keyman for Windows by monitoring the health of the low level keyboard hook. If keyman.exe is unresponsive at any time, Windows can silently uninstall its low level keyboard hook, which results in (at least) two problems: * Keyman's hotkeys stop working * A modifier key can become stuck, if it was pressed around the time Keyman became unresponsive. The most common scenario in which Keyman can become unresponsive is high system load, e.g. rendering graphics, videoconference calls, compiling software. Restarting Keyman always resolved both of these two issues in the past, but with this patch, I hope that this will no longer be necessary. A related 'fakefreeze' project is included for simulating a keyman.exe hang by posting a `wm_keyman_control:KMC_WATCHDOG_FAKEFREEZE` message to it, which keyman.exe responds to by `Sleep()`ing for 5 seconds. While keyman.exe is freezing, any keystroke will cause the low level hook to be uninstalled by Windows. Logging has been updated; look for "LowLevelHookWatchDog" in the log for related events. One final small change in keyman32.cpp, as I refactored the WH_KEYBOARD_LL hook installation/uninstallation, was to always clear out hook variables when uninstalling a hook, because if the hook fails to uninstall, there's really nothing we can do about it anyway, and we probably shouldn't be trying again. Fixes: #8064 --- windows/src/engine/keyman/UfrmKeyman7Main.pas | 17 +++ .../engine/keyman32/LowLevelHookWatchDog.cpp | 105 ++++++++++++++ .../engine/keyman32/LowLevelHookWatchDog.h | 32 +++++ .../keyman32/k32_lowlevelkeyboardhook.cpp | 2 + windows/src/engine/keyman32/keyman32.cpp | 52 +++++-- windows/src/engine/keyman32/keyman32.def | 2 + windows/src/engine/keyman32/keyman32.vcxproj | 2 + windows/src/engine/keyman32/keymancontrol.h | 6 + windows/src/engine/keyman32/keymanengine.h | 3 + .../src/engine/keyman32/kmhook_getmessage.cpp | 9 ++ .../kmcomapi/com/system/keymancontrol.pas | 25 ++++ .../delphi/general/KeymanControlMessages.pas | 3 + .../delphi/general/KeymanEngineControl.pas | 18 ++- windows/src/support/fakefreeze/.gitignore | 1 + windows/src/support/fakefreeze/fakefreeze.cpp | 39 ++++++ windows/src/support/fakefreeze/fakefreeze.sln | 31 +++++ .../src/support/fakefreeze/fakefreeze.vcxproj | 131 ++++++++++++++++++ .../fakefreeze/fakefreeze.vcxproj.filters | 22 +++ 18 files changed, 484 insertions(+), 16 deletions(-) create mode 100644 windows/src/engine/keyman32/LowLevelHookWatchDog.cpp create mode 100644 windows/src/engine/keyman32/LowLevelHookWatchDog.h create mode 100644 windows/src/support/fakefreeze/.gitignore create mode 100644 windows/src/support/fakefreeze/fakefreeze.cpp create mode 100644 windows/src/support/fakefreeze/fakefreeze.sln create mode 100644 windows/src/support/fakefreeze/fakefreeze.vcxproj create mode 100644 windows/src/support/fakefreeze/fakefreeze.vcxproj.filters diff --git a/windows/src/engine/keyman/UfrmKeyman7Main.pas b/windows/src/engine/keyman/UfrmKeyman7Main.pas index 7f948d0518..a33e69cea4 100644 --- a/windows/src/engine/keyman/UfrmKeyman7Main.pas +++ b/windows/src/engine/keyman/UfrmKeyman7Main.pas @@ -292,6 +292,8 @@ type procedure UnregisterControllerWindows; // I4731 function IsSysTrayWindow(AHandle: THandle): Boolean; procedure GetTrayIconHandle; // I4731 + procedure WatchDogKeyEvent; + protected procedure DoInterfaceHotkey(Target: Integer); @@ -849,6 +851,14 @@ begin if (TKeymanHint(lParam) = KH_EXITPRODUCT) and (wParam = mrOk) then UnloadProduct; end; + KMC_WATCHDOG_KEYEVENT: + WatchDogKeyEvent; + KMC_WATCHDOG_FAKEFREEZE: + begin + TDebugLogClient.Instance.WriteMessage('kmc_fakefreeze begin', []); + Sleep(5000); + TDebugLogClient.Instance.WriteMessage('kmc_fakefreeze end', []); + end; //TOUCH KMC_CONTEXT: //TOUCH begin //TOUCH if LParam <> 0 then @@ -857,6 +867,13 @@ begin end; end; +procedure TfrmKeyman7Main.WatchDogKeyEvent; +begin + TDebugLogClient.Instance.WriteMessage('Attempting to send WatchDogKeyEvent', []); + if kmint.KeymanEngineControl <> nil then + kmint.KeymanEngineControl.WatchDogKeyEvent; +end; + procedure TfrmKeyman7Main.DoInterfaceHotkey(Target: Integer); begin if not Assigned(FRunningProduct) then diff --git a/windows/src/engine/keyman32/LowLevelHookWatchDog.cpp b/windows/src/engine/keyman32/LowLevelHookWatchDog.cpp new file mode 100644 index 0000000000..3da1c11feb --- /dev/null +++ b/windows/src/engine/keyman32/LowLevelHookWatchDog.cpp @@ -0,0 +1,105 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-11-17 + * + * Handle situations where Windows silently uninstalls our low level keyboard + * hook, and dynamically reinstall it. The hook can be uninstalled when + * keyman.exe becomes unresponsive for more than 200msec (default timeout) -- + * this can be due to something Keyman is doing, but it could also happen during + * high system load. The hook will only be uninstalled if a key is pressed while + * Keyman is unresponsive. + * + * Per Windows documentation: + * https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc#remarks + * + * This works by tracking last event times for both the WH_KEYBOARD_LL hook and + * the WH_GETMESSAGE hook. These two hooks both receive key events, but the + * WH_GETMESSAGE hook runs in the focused thread context, whereas WH_KEYBOARD_LL + * runs in keyman.exe main thread context. The WH_KEYBOARD_LL hook receives the + * key event first. + * + * This (static) class is only used in the keyman.exe main thread context. + * + * Notification of HookIsAlive is a straightforward call from the low level + * keyboard hook procedure, but the KeyEventReceivedInGetMessageProc function + * must be signalled across processes. We have chosen to do this with a posted + * message to the master controller, which is then handled by + * UfrmKeyman7Main.pas, and passed through Keyman_WatchDogKeyEvent. + * + * The message handler was implemented in UfrmKeyman7Main.pas rather than in + * kmhook_getmessage.cpp, because it appears that kmnGetMessageProc does not see + * messages that were posted by it or child functions (this is by observation, not + * documentation -- I was not able to find documentation on this, but surmise it + * is to prevent deadlock/infinite loop scenarios, which could easily lockup + * Windows entirely). + * + * No key data is passed in the event, only the information that a key was + * pressed. + */ + +#include "pch.h" + +/** + * minimum number of milliseconds between the last LowLevel and GetMessage + * events before we assume the low level hook has been uninstalled, and we need + * to reinstall it. + */ +#define WATCHDOG_THRESHOLD 1000 + +static ULONGLONG LastLowLevelEventTick = 0; +static ULONGLONG LastGetMessageEventTick = 0; + +void LowLevelHookWatchDog::HookIsAlive() { + // ULONGLONG Previous = LastLowLevelEventTick; + LastLowLevelEventTick = GetTickCount64(); + // SendDebugMessageFormat("LowLevelHookWatchDog::HookIsAlive currentLL=%llu currentGM=%llu (lastLL=%llu)", LastLowLevelEventTick, LastGetMessageEventTick, Previous); +} + +void LowLevelHookWatchDog::KeyEventReceivedInGetMessageProc() { + // ULONGLONG Previous = LastGetMessageEventTick; + LastGetMessageEventTick = GetTickCount64(); + // SendDebugMessageFormat("LowLevelHookWatchDog::KeyEventReceivedInGetMessageProc currentLL=%llu currentGM=%llu (lastGM=%llu)", LastLowLevelEventTick, LastGetMessageEventTick, Previous); + + // This is a good place to check if we are still alive -- shortly after each + // keystroke event in the GetMessage hook, as this means at worst we'll have + // one or two keystrokes where Keyman must recover + if(!CheckIfHookIsAlive()) { + ReinstallHook(); + } +} + +bool LowLevelHookWatchDog::CheckIfHookIsAlive() { + if(LastGetMessageEventTick < LastLowLevelEventTick) { + // this shouldn't be possible but rather safe than sorry + return true; + } + + return LastGetMessageEventTick - LastLowLevelEventTick < WATCHDOG_THRESHOLD; +} + +void LowLevelHookWatchDog::ReinstallHook() { + //keyman32.cpp: + SendDebugMessageFormat( + "Attempting to reinstall hook because watchdog threshold exceeded by %llu msec (last LL=%llu last GM=%llu)", + LastGetMessageEventTick - LastLowLevelEventTick, + LastLowLevelEventTick, + LastGetMessageEventTick + ); + + if(!RestartLowLevelHook()) { + SendDebugMessage("Attempt to reinstall low level hook may have failed, see previous log messages"); + } else { + SendDebugMessage("Attempt to reinstall low level hook succeeded"); + } + + // We should assume the hook is alive at this point to avoid repeated resets + HookIsAlive(); +} + +#ifndef _WIN64 +extern "C" void __declspec(dllexport) WINAPI Keyman_WatchDogKeyEvent() { + SendDebugMessageFormat("Keyman_WatchDogKeyEvent"); + LowLevelHookWatchDog::KeyEventReceivedInGetMessageProc(); +} +#endif \ No newline at end of file diff --git a/windows/src/engine/keyman32/LowLevelHookWatchDog.h b/windows/src/engine/keyman32/LowLevelHookWatchDog.h new file mode 100644 index 0000000000..cd2ea07ff1 --- /dev/null +++ b/windows/src/engine/keyman32/LowLevelHookWatchDog.h @@ -0,0 +1,32 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-11-17 + */ + +/** + * @brief Watch for scenarios where the low level keyboard hook may be + * uninstalled by Windows, e.g. when there is heavy system load, and + * reinstall it. + */ +class LowLevelHookWatchDog { +public: + /** + * @brief Update the watchdog timestamp to current time, because the low + * level hook is receiving messages successfully + */ + static void HookIsAlive(); + + /** + * @brief Update the watchdog GetMessageProc timestamp to current time; + * this is called by the Keyman_WatchDogKeyEvent function, when + * a message posted from the GetMessageProc hook is received by + * keyman.exe. + */ + static void KeyEventReceivedInGetMessageProc(); + +private: + static bool CheckIfHookIsAlive(); + static void ReinstallHook(); +}; + diff --git a/windows/src/engine/keyman32/k32_lowlevelkeyboardhook.cpp b/windows/src/engine/keyman32/k32_lowlevelkeyboardhook.cpp index 47d77fc1ad..4b40846b93 100644 --- a/windows/src/engine/keyman32/k32_lowlevelkeyboardhook.cpp +++ b/windows/src/engine/keyman32/k32_lowlevelkeyboardhook.cpp @@ -141,6 +141,8 @@ LRESULT _kmnLowLevelKeyboardProc( SendDebugEntry(); + LowLevelHookWatchDog::HookIsAlive(); + PKBDLLHOOKSTRUCT hs = (PKBDLLHOOKSTRUCT) lParam; BOOL extended = hs->flags & LLKHF_EXTENDED ? TRUE : FALSE; diff --git a/windows/src/engine/keyman32/keyman32.cpp b/windows/src/engine/keyman32/keyman32.cpp index aadd695906..6c8f34116f 100644 --- a/windows/src/engine/keyman32/keyman32.cpp +++ b/windows/src/engine/keyman32/keyman32.cpp @@ -279,6 +279,24 @@ extern "C" BOOL _declspec(dllexport) WINAPI Keyman_GetInitialised(BOOL *FSingleA return Globals::get_Keyman_Initialised(); } +#ifndef _WIN64 +BOOL InitLowLevelHook() { + HINSTANCE hinst = GetModuleHandle(LIBRARY_NAME); + + *Globals::hhookLowLevelKeyboardProc() = SetWindowsHookExW(WH_KEYBOARD_LL, (HOOKPROC) kmnLowLevelKeyboardProc, hinst, Globals::get_FSingleThread()); // I4124 + return Globals::get_hhookLowLevelKeyboardProc() != NULL; +} + +BOOL UninitLowLevelHook() { + BOOL RetVal = TRUE; + if(Globals::get_hhookLowLevelKeyboardProc() && !UnhookWindowsHookEx(Globals::get_hhookLowLevelKeyboardProc())) // I4124 + RetVal = FALSE; + + *Globals::hhookLowLevelKeyboardProc() = NULL; + return RetVal; +} +#endif + BOOL InitHooks() { HINSTANCE hinst = GetModuleHandle(LIBRARY_NAME); @@ -286,7 +304,7 @@ BOOL InitHooks() *Globals::hhookGetMessage() = SetWindowsHookExW(WH_GETMESSAGE, (HOOKPROC) kmnGetMessageProc, hinst, Globals::get_FSingleThread()); *Globals::hhookCallWndProc() = SetWindowsHookExW(WH_CALLWNDPROC, (HOOKPROC) kmnCallWndProc, hinst, Globals::get_FSingleThread()); #ifndef _WIN64 - *Globals::hhookLowLevelKeyboardProc() = SetWindowsHookExW(WH_KEYBOARD_LL, (HOOKPROC) kmnLowLevelKeyboardProc, hinst, Globals::get_FSingleThread()); // I4124 + InitLowLevelHook(); #endif; return @@ -304,19 +322,14 @@ BOOL UninitHooks() if(Globals::get_hhookGetMessage() && !UnhookWindowsHookEx(Globals::get_hhookGetMessage())) RetVal = FALSE; - else - *Globals::hhookGetMessage() = NULL; + *Globals::hhookGetMessage() = NULL; if(Globals::get_hhookCallWndProc() && !UnhookWindowsHookEx(Globals::get_hhookCallWndProc())) RetVal = FALSE; - else - *Globals::hhookCallWndProc() = NULL; + *Globals::hhookCallWndProc() = NULL; #ifndef _WIN64 - if(Globals::get_hhookLowLevelKeyboardProc() && !UnhookWindowsHookEx(Globals::get_hhookLowLevelKeyboardProc())) // I4124 - RetVal = FALSE; - else - *Globals::hhookLowLevelKeyboardProc() = NULL; + RetVal = UninitLowLevelHook() && RetVal; #endif return RetVal; @@ -473,6 +486,27 @@ extern "C" BOOL _declspec(dllexport) WINAPI Keyman_RestartEngine() return TRUE; } +BOOL RestartLowLevelHook() { + BOOL result=true; + +#ifndef _WIN64 + if(!Globals::get_Keyman_Initialised()) { + return FALSE; + } + + if(!UninitLowLevelHook()) { + SendDebugMessageFormat("Failed to uninstall low level hook. GetLastError = %d", GetLastError()); + result = FALSE; + } + if(!InitLowLevelHook()) { + SendDebugMessageFormat("Failed to install low level hook. GetLastError = %d", GetLastError()); + result = FALSE; + } + +#endif + return result; +} + //--------------------------------------------------------------------------------------------------------- // // Utility guff functions diff --git a/windows/src/engine/keyman32/keyman32.def b/windows/src/engine/keyman32/keyman32.def index a61a78b463..f457075ce8 100644 --- a/windows/src/engine/keyman32/keyman32.def +++ b/windows/src/engine/keyman32/keyman32.def @@ -49,3 +49,5 @@ EXPORTS Keyman_UnregisterControllerThread SetCustomPostKeyCallback + + Keyman_WatchDogKeyEvent diff --git a/windows/src/engine/keyman32/keyman32.vcxproj b/windows/src/engine/keyman32/keyman32.vcxproj index ba15db6516..9d02f8dacd 100644 --- a/windows/src/engine/keyman32/keyman32.vcxproj +++ b/windows/src/engine/keyman32/keyman32.vcxproj @@ -361,6 +361,7 @@ + Create Create @@ -450,6 +451,7 @@ + diff --git a/windows/src/engine/keyman32/keymancontrol.h b/windows/src/engine/keyman32/keymancontrol.h index 70a0c5d39e..73f30f77de 100644 --- a/windows/src/engine/keyman32/keymancontrol.h +++ b/windows/src/engine/keyman32/keymancontrol.h @@ -47,6 +47,12 @@ #define KMC_PROFILECHANGED 18 // 9.0.426.0 // I3933 +#define KMC_HINTRESPONSE 19 // 14.0 HIWORD(wParam) = ModalResult, lParam = hint enum + +#define KMC_WATCHDOG_FAKEFREEZE 20 // 19.0 - pause Keyman for 5 seconds for debug purposes to test stability +#define KMC_WATCHDOG_KEYEVENT 21 // 19.0 - let the LowLevelHookWatchDog know that input has happened on another thread + + #define PC_UPDATE 0 // Tell Keyman to update its display of active keyboard #define PC_UPDATE_LANGUAGESWITCH 1 // Tell Keyman to update its display of active keyboard and then open language switch form #define PC_HOTKEYCHANGE 2 // Tell Keyman that a hotkey was pressed to switch keyboard diff --git a/windows/src/engine/keyman32/keymanengine.h b/windows/src/engine/keyman32/keymanengine.h index c784b04897..aa8f6ffa30 100644 --- a/windows/src/engine/keyman32/keymanengine.h +++ b/windows/src/engine/keyman32/keymanengine.h @@ -263,6 +263,7 @@ void keybd_shift(LPINPUT pInputs, int* n, BOOL isReset, LPBYTE const kbd); #include "k32_tsf.h" #include "k32_visualkeyboardinterface.h" +#include "LowLevelHookWatchDog.h" void ReportActiveKeyboard(WORD wCommand); // I3933 // I3949 void SelectKeyboardHKL(DWORD hkl, BOOL foreground); // I3933 // I3949 // I4271 @@ -273,4 +274,6 @@ void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended); // I4793 BOOL SetupCoreEnvironment(km_core_option_item **test_env_opts); void DeleteCoreEnvironment(km_core_option_item *test_env_opts); +BOOL RestartLowLevelHook(); + #endif // _KEYMANENGINE_H diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 5c7af7ce8a..4fee835254 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -146,6 +146,15 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) } if ((mp->message == WM_KEYDOWN || mp->message == WM_SYSKEYDOWN || mp->message == WM_KEYUP || mp->message == WM_SYSKEYUP)) { // I4642 + + // To help our low level keyboard hook recover if system load causes it to + // be uninstalled, we tell the controller process that we should have seen a + // keystroke event in the low level keyboard hook; this gets passed into the + // LowLevelHookWatchDog via keyman.exe + if(mp->message == WM_KEYDOWN || mp->message == WM_SYSKEYDOWN) { + Globals::PostMasterController(wm_keyman_control, KMC_WATCHDOG_KEYEVENT, 0); + } + BYTE scan = KEYMSG_LPARAM_SCAN(mp->lParam); CheckScheduledRefresh(); _td->LastScanCode = scan; diff --git a/windows/src/engine/kmcomapi/com/system/keymancontrol.pas b/windows/src/engine/kmcomapi/com/system/keymancontrol.pas index 90bab26746..ef6fa8be83 100644 --- a/windows/src/engine/kmcomapi/com/system/keymancontrol.pas +++ b/windows/src/engine/kmcomapi/com/system/keymancontrol.pas @@ -76,6 +76,7 @@ type TKeyman32ControllerSendMessageFunction = function(msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; TKeyman32ControllerPostMessageFunction = procedure(msg: UINT; wParam: WPARAM; lParam: LPARAM); stdcall; TKeyman32UpdateTouchPanelVisibilityFunction = procedure(Value: BOOL); stdcall; + TKeyman32WatchDogKeyEvent = procedure; stdcall; TKeymanControl = class(TKeymanAutoObject, IKeymanCustomisationAccess, IIntKeymanControl, IKeymanControl, IKeymanEngineControl) private @@ -97,6 +98,7 @@ type FKeyman_PostMasterController: TKeyman32ControllerPostMessageFunction; FKeyman_StartExit: TKeyman32ExitFunction; // I3092 FKeyman_UpdateTouchPanelVisibility: TKeyman32UpdateTouchPanelVisibilityFunction; + FKeyman_WatchDogKeyEvent: TKeyman32WatchDogKeyEvent; procedure LoadKeyman32; procedure StartKeyman32; procedure Do_Keyman_Exit; @@ -148,6 +150,7 @@ type procedure DisableUserInterface; safecall; procedure EnableUserInterface; safecall; procedure UpdateTouchPanelVisibility(Value: Boolean); safecall; + procedure WatchDogKeyEvent; safecall; // 32 bit only procedure DiagnosticTestException; safecall; @@ -524,6 +527,25 @@ begin {$ENDIF} end; +(** + * Tell the LowLevelHookWatchDog that a key event has been received + *) +procedure TKeymanControl.WatchDogKeyEvent; +begin +{$IFDEF WIN64} + Error(Cardinal(E_NOTIMPL)); +{$ELSE} + KL.MethodEnter(Self, 'WatchDogKeyEvent', []); + try + LoadKeyman32; + FKeyman_WatchDogKeyEvent; + finally + KL.MethodExit(Self, 'WatchDogKeyEvent'); + end; +{$ENDIF} +end; + + procedure TKeymanControl.RegisterControllerThread(Value: LongWord); begin {$IFDEF WIN64} @@ -750,6 +772,9 @@ begin @FKeyman_SendMasterController := ProcAddr('Keyman_SendMasterController'); @FKeyman_PostMasterController := ProcAddr('Keyman_PostMasterController'); @FKeyman_UpdateTouchPanelVisibility := ProcAddr('Keyman_UpdateTouchPanelVisibility'); +{$IFNDEF WIN64} + @FKeyman_WatchDogKeyEvent := ProcAddr('Keyman_WatchDogKeyEvent'); +{$ENDIF} end; end; diff --git a/windows/src/global/delphi/general/KeymanControlMessages.pas b/windows/src/global/delphi/general/KeymanControlMessages.pas index 0fd5a6a1b5..67712e1747 100644 --- a/windows/src/global/delphi/general/KeymanControlMessages.pas +++ b/windows/src/global/delphi/general/KeymanControlMessages.pas @@ -54,6 +54,9 @@ const KMC_HINTRESPONSE = 19; // 14.0 HIWORD(wParam) = ModalResult, lParam = hint enum + KMC_WATCHDOG_FAKEFREEZE = 20; // 19.0 - pause Keyman for 5 seconds for debug purposes to test stability + KMC_WATCHDOG_KEYEVENT = 21; // 19.0 - let the LowLevelHookWatchDog know that input has happened on another thread + PC_UPDATE = 0; PC_UPDATE_LANGUAGESWITCH = 1; PC_HOTKEYCHANGE = 2; diff --git a/windows/src/global/delphi/general/KeymanEngineControl.pas b/windows/src/global/delphi/general/KeymanEngineControl.pas index 5afd6f9013..762755c172 100644 --- a/windows/src/global/delphi/general/KeymanEngineControl.pas +++ b/windows/src/global/delphi/general/KeymanEngineControl.pas @@ -1,18 +1,18 @@ (* Name: KeymanControlRestart Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 19 Jun 2007 Modified Date: 19 Jun 2007 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 19 Jun 2007 - mcdurdin - Initial version *) unit KeymanEngineControl; @@ -43,6 +43,10 @@ type procedure UnregisterMasterController(Value: LongWord); safecall; procedure RegisterControllerThread(Value: LongWord); safecall; procedure UnregisterControllerThread(Value: LongWord); safecall; + + // New in 19.0 + + procedure WatchDogKeyEvent; safecall; // 32 bit only end; implementation diff --git a/windows/src/support/fakefreeze/.gitignore b/windows/src/support/fakefreeze/.gitignore new file mode 100644 index 0000000000..2959b40eec --- /dev/null +++ b/windows/src/support/fakefreeze/.gitignore @@ -0,0 +1 @@ +x64/ diff --git a/windows/src/support/fakefreeze/fakefreeze.cpp b/windows/src/support/fakefreeze/fakefreeze.cpp new file mode 100644 index 0000000000..4e78f43657 --- /dev/null +++ b/windows/src/support/fakefreeze/fakefreeze.cpp @@ -0,0 +1,39 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-11-17 + * + * Tell keyman.exe to pause for 5 seconds to force Windows to silently uninstall + * the low level keyboard hook, per Windows documentation at + * https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc#remarks + * + * See LowLevelHookWatchDog.cpp for more detail + */ +#include +#include +#include + +#define KMC_WATCHDOG_FAKEFREEZE 20 + +int main() { + std::cout << "Posting a freeze message to Keyman master controller\n"; + UINT wm_keyman_control = RegisterWindowMessage(L"WM_KEYMAN_CONTROL"); + HWND hwnd = FindWindow(L"TfrmKeyman7Main", NULL); + if (hwnd == NULL) { + std::cout << "Keyman master controller window not found\n"; + return 1; + } + + if (!PostMessage(hwnd, wm_keyman_control, KMC_WATCHDOG_FAKEFREEZE, 0)) { + std::cout << "Error calling Keyman KMC_WATCHDOG_FAKEFREEZE\n"; + } + std::cout << "Sleeping 5 seconds...\n"; + + for (int i = 1; i <= 5; i++) { + Sleep(1000); + std::cout << "..." << i << "\n"; + } + std::cout << "Keyman should be responsive again now\n"; + + return 0; +} diff --git a/windows/src/support/fakefreeze/fakefreeze.sln b/windows/src/support/fakefreeze/fakefreeze.sln new file mode 100644 index 0000000000..f9c6b43a48 --- /dev/null +++ b/windows/src/support/fakefreeze/fakefreeze.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36616.10 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "fakefreeze", "fakefreeze.vcxproj", "{BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Debug|x64.ActiveCfg = Debug|x64 + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Debug|x64.Build.0 = Debug|x64 + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Debug|x86.ActiveCfg = Debug|Win32 + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Debug|x86.Build.0 = Debug|Win32 + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Release|x64.ActiveCfg = Release|x64 + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Release|x64.Build.0 = Release|x64 + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Release|x86.ActiveCfg = Release|Win32 + {BDDE094F-B409-4DF5-AA9E-6DEC09B362CE}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA12682B-94DC-4E10-AE3F-743D64317F12} + EndGlobalSection +EndGlobal diff --git a/windows/src/support/fakefreeze/fakefreeze.vcxproj b/windows/src/support/fakefreeze/fakefreeze.vcxproj new file mode 100644 index 0000000000..bdc42144ea --- /dev/null +++ b/windows/src/support/fakefreeze/fakefreeze.vcxproj @@ -0,0 +1,131 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {bdde094f-b409-4df5-aa9e-6dec09b362ce} + fakefreeze + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + + + + + \ No newline at end of file diff --git a/windows/src/support/fakefreeze/fakefreeze.vcxproj.filters b/windows/src/support/fakefreeze/fakefreeze.vcxproj.filters new file mode 100644 index 0000000000..8b54f2cabc --- /dev/null +++ b/windows/src/support/fakefreeze/fakefreeze.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file