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