From 6155b544ea46bb50a8669efc7c7765c872c4c7b4 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 15 Mar 2021 09:12:36 +1100 Subject: [PATCH] fix(windows): Trigger language sync after changes Fixes #4447. Fixes #4222. The symptoms for these two issues are related: the language associations change and keyboards stop functioning correctly. The issue is described in detail in #4447. Although this fix should be considered 'experimental', we should probably include it in the release of 14.0, because it fixes a longstanding issue with Keyman and Windows languages. --- windows/src/engine/engine.sln | 10 ++ windows/src/engine/inst/components.wxs | 8 + .../keyboards/keymankeyboardsinstalled.pas | 45 +++++ windows/src/engine/kmrefresh/Makefile | 40 +++++ windows/src/engine/kmrefresh/kmrefresh.cpp | 74 +++++++++ .../src/engine/kmrefresh/kmrefresh.vcxproj | 155 ++++++++++++++++++ .../kmrefresh/kmrefresh.vcxproj.filters | 27 +++ windows/src/engine/kmrefresh/version.in | 30 ++++ .../delphi/general/Keyman.System.Settings.pas | 16 +- .../global/delphi/general/RegistryKeys.pas | 1 + .../src/global/delphi/general/utilexecute.pas | 35 ++++ 11 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 windows/src/engine/kmrefresh/Makefile create mode 100644 windows/src/engine/kmrefresh/kmrefresh.cpp create mode 100644 windows/src/engine/kmrefresh/kmrefresh.vcxproj create mode 100644 windows/src/engine/kmrefresh/kmrefresh.vcxproj.filters create mode 100644 windows/src/engine/kmrefresh/version.in diff --git a/windows/src/engine/engine.sln b/windows/src/engine/engine.sln index 79ca6fc1cf..a17faff3b1 100644 --- a/windows/src/engine/engine.sln +++ b/windows/src/engine/engine.sln @@ -13,6 +13,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "kmtip", "kmtip\kmtip.vcxpro EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mcompile", "mcompile\mcompile.vcxproj", "{5C22DDDA-CEAD-45F1-96B0-1473111156AA}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "kmrefresh", "kmrefresh\kmrefresh.vcxproj", "{A3B8F75F-E63E-434E-8A2A-034AD9B66571}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -53,6 +55,14 @@ Global {5C22DDDA-CEAD-45F1-96B0-1473111156AA}.Release|x64.ActiveCfg = Release|Win32 {5C22DDDA-CEAD-45F1-96B0-1473111156AA}.Release|x86.ActiveCfg = Release|Win32 {5C22DDDA-CEAD-45F1-96B0-1473111156AA}.Release|x86.Build.0 = Release|Win32 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Debug|x64.ActiveCfg = Debug|x64 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Debug|x64.Build.0 = Debug|x64 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Debug|x86.ActiveCfg = Debug|Win32 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Debug|x86.Build.0 = Debug|Win32 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Release|x64.ActiveCfg = Release|x64 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Release|x64.Build.0 = Release|x64 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Release|x86.ActiveCfg = Release|Win32 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/windows/src/engine/inst/components.wxs b/windows/src/engine/inst/components.wxs index e21d6b3652..4d842b3ef1 100644 --- a/windows/src/engine/inst/components.wxs +++ b/windows/src/engine/inst/components.wxs @@ -156,6 +156,14 @@ /> + + + + + + + + diff --git a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas index 0ca0d403ee..06d523d341 100644 --- a/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas +++ b/windows/src/engine/kmcomapi/com/keyboards/keymankeyboardsinstalled.pas @@ -40,6 +40,7 @@ type TKeymanKeyboardsInstalled = class(TKeymanAutoCollectionObject, IKeymanKeyboardsInstalled, IKeymanKeyboardsInstalled2, IIntKeymanKeyboardsInstalled) // I4376 private FKeyboards: TKeyboardList; + procedure TriggerWindowsLanguageSync; protected procedure DoRefresh; override; @@ -69,14 +70,18 @@ uses System.Variants, Winapi.msctf, custinterfaces, + DebugPaths, keymankeyboardfile, keymanpackageinstalled, keymanpackagesinstalled, + KeymanPaths, KLog, kpinstallkeyboard, keyman_msctf, OnlineConstants, regkeyboards, + RegistryKeys, + utilexecute, utilfiletypes; constructor TKeymanKeyboardsInstalled.Create(AContext: TKeymanContext); @@ -182,6 +187,46 @@ begin end; Context.Control.AutoApplyKeyman; + + TriggerWindowsLanguageSync; +end; + +// This triggers a synchronisation of the language settings to the +// cloud, which avoids an issue where language setting changes are +// overwritten on a subsequent reboot of the system. +// +// See #4447 and kmrefresh.cpp for additional detail. +// +// Although the sync function is not supported on Windows 7, 8.1, +// we'll call kmrefresh.exe anyway, as we cannot be sure which version +// of Windows we are running on -- the calling process may be lied to +// if it does not have an appropriate manifest. kmrefresh.exe is safe +// to run as it will simply exit if it does not find the appropriate +// endpoint to call. +// +// On x64 systems, we must call the x64 version of the process, as the +// x86 version does not appear to work. +procedure TKeymanKeyboardsInstalled.TriggerWindowsLanguageSync; +var + processName: string; + FWow64: LongBool; +begin + if not Reg_GetDebugFlag(SRegValue_Flag_SyncLanguagesToCloud, True) then + Exit; + +{$IFDEF WIN64} + processName := 'kmrefresh.x64.exe'; +{$ELSE} + if IsWow64Process(GetCurrentProcess, FWow64) and FWow64 + then processName := 'kmrefresh.x64.exe' + else processName := 'kmrefresh.x86.exe'; +{$ENDIF} + + TUtilExecute.Execute( + TKeymanPaths.KeymanEngineInstallPath(processName), + ExtractFileDir(ParamStr(0)), + SW_HIDE + ); end; procedure TKeymanKeyboardsInstalled.DoRefresh; diff --git a/windows/src/engine/kmrefresh/Makefile b/windows/src/engine/kmrefresh/Makefile new file mode 100644 index 0000000000..a3b676ae52 --- /dev/null +++ b/windows/src/engine/kmrefresh/Makefile @@ -0,0 +1,40 @@ +# +# kmrefresh Makefile +# + +!include ..\..\Defines.mak + +PLATFORM=x64 + +build: version.res dirs + $(MSBUILD) kmrefresh.vcxproj /p:Platform=Win32 $(MSBUILD_BUILD) + $(MSBUILD) kmrefresh.vcxproj /p:Platform=x64 $(MSBUILD_BUILD) + $(COPY) $(WIN32_TARGET_PATH)\kmrefresh.x86.exe $(PROGRAM)\engine + $(COPY) $(WIN32_TARGET_PATH)\kmrefresh.x86.pdb $(DEBUGPATH)\engine + $(COPY) $(X64_TARGET_PATH)\kmrefresh.x64.exe $(PROGRAM)\engine + $(COPY) $(X64_TARGET_PATH)\kmrefresh.x64.pdb $(DEBUGPATH)\engine + +clean: def-clean + $(MSBUILD) $(MSBUILD_CLEAN) kmrefresh.vcxproj + -rd /s/q x64 + -rd /s/q Win32 + +signcode: + $(SIGNCODE) /d "Keyman Engine Refresh x86" $(PROGRAM)\engine\kmrefresh.x86.exe + $(SIGNCODE) /d "Keyman Engine Refresh x64" $(PROGRAM)\engine\kmrefresh.x64.exe + +wrap-symbols: + $(SYMSTORE) $(PROGRAM)\engine\kmrefresh.x86.exe /t keyman-engine-windows + $(SYMSTORE) $(DEBUGPATH)\engine\kmrefresh.x86.pdb /t keyman-engine-windows + $(SYMSTORE) $(PROGRAM)\engine\kmrefresh.x64.exe /t keyman-engine-windows + $(SYMSTORE) $(DEBUGPATH)\engine\kmrefresh.x64.pdb /t keyman-engine-windows + +backup: + $(WZZIP) $(BUILD)\engine\kmrefresh.x86.exe $(BACKUPDEFAULTS) kmrefresh.x86.exe + $(WZZIP) $(BUILD)\engine\kmrefresh.x64.exe $(BACKUPDEFAULTS) kmrefresh.x64.exe + +install: + $(COPY) $(PROGRAM)\engine\kmrefresh.x86.exe "$(INSTALLPATH_KEYMANENGINE)" + $(COPY) $(PROGRAM)\engine\kmrefresh.x64.exe "$(INSTALLPATH_KEYMANENGINE)" + +!include ..\..\Target.mak diff --git a/windows/src/engine/kmrefresh/kmrefresh.cpp b/windows/src/engine/kmrefresh/kmrefresh.cpp new file mode 100644 index 0000000000..8647f9b4aa --- /dev/null +++ b/windows/src/engine/kmrefresh/kmrefresh.cpp @@ -0,0 +1,74 @@ +/* + kmrefresh triggers a synchronisation of Windows language data to + Microsoft's 'cloud'. This process is required to ensure that Windows + does not stomp over the current language configuration when it + attempts to synchronise the data back from the cloud, for instance + shortly after logging into the system. + + This cloud functionality was introduced in Windows 10 1809? It is + not present in Windows 7, 8.1 or Windows 10 RTM. The process should + run without issues on all platforms, however, and just be a no-op + where the endpoint is not available. + + The SyncLanguageDataToCloud API endpoint is currently undocumented, + so this should be considered experimental. Alternative approaches + have been considered in corresponding issue #4447, but this is both + lightweight and respectful of user preferences, so this is the + approach we've opted to take for now. Plus, it works really well. + + One trick to be aware of is that the sync function fails if run from + WoW64. So make sure you run the version with the correct bitness. + I've put this check into the caller app (kmcomapi) rather than in + this process. kmcomapi does not currently check exit codes but you + could track them e.g. with procmon if you wanted to verify behaviour. + + See #4447 for additional information. +*/ +#define STRICT +#include + +typedef void (WINAPI * PSYNCLANGUAGEDATATOCLOUD)(); + +int APIENTRY wWinMain(HINSTANCE hInstance, + HINSTANCE hPrevInstance, + PWSTR lpCmdLine, + int nCmdShow) +{ + UNREFERENCED_PARAMETER(hPrevInstance); + UNREFERENCED_PARAMETER(hInstance); + UNREFERENCED_PARAMETER(lpCmdLine); + UNREFERENCED_PARAMETER(nCmdShow); + + int nExitCode = 0; + + if(!SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) + return 1; + + HMODULE handle = LoadLibrary(L"coreglobconfig.dll"); + if (handle == NULL) { + CoUninitialize(); + return 2; + } + + PSYNCLANGUAGEDATATOCLOUD pSync = (PSYNCLANGUAGEDATATOCLOUD)GetProcAddress(handle, "SyncLanguageDataToCloud"); + if (pSync == NULL) { + FreeLibrary(handle); + CoUninitialize(); + return 3; + } + + pSync(); + + // The call to SyncLanguageDataToCloud triggers a worker thread to do + // the work. As of writing, I am unaware of a way to determine that the + // worker thread has completed, so the sleep gives plenty of time for + // it to finish. This process is invisible and silent so the fact that + // it lives in the background for a few seconds should not be a problem. + + Sleep(10000); + + FreeLibrary(handle); + CoUninitialize(); + + return 0; +} diff --git a/windows/src/engine/kmrefresh/kmrefresh.vcxproj b/windows/src/engine/kmrefresh/kmrefresh.vcxproj new file mode 100644 index 0000000000..2a24a41df7 --- /dev/null +++ b/windows/src/engine/kmrefresh/kmrefresh.vcxproj @@ -0,0 +1,155 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {A3B8F75F-E63E-434E-8A2A-034AD9B66571} + kmrefresh + 10.0.19041.0 + + + + Application + true + Unicode + v141 + + + Application + false + true + Unicode + v141 + + + Application + true + Unicode + v141 + + + Application + false + true + Unicode + v141 + + + + + + + + + + + + + + + + + + + + + $(Platform)\$(Configuration)\ + $(ProjectName).x64 + + + $(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + $(ProjectName).x86 + + + $(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + $(ProjectName).x86 + + + $(Platform)\$(Configuration)\ + $(ProjectName).x64 + + + + Level3 + Disabled + true + true + MultiThreadedDebug + + + Windows + + + + + Level3 + Disabled + true + true + MultiThreadedDebug + + + Windows + + + + + Level3 + MaxSpeed + true + true + true + true + MultiThreaded + + + Windows + true + true + + + + + Level3 + MaxSpeed + true + true + true + true + MultiThreaded + + + Windows + true + true + + + + + + + + + + + + \ No newline at end of file diff --git a/windows/src/engine/kmrefresh/kmrefresh.vcxproj.filters b/windows/src/engine/kmrefresh/kmrefresh.vcxproj.filters new file mode 100644 index 0000000000..88abd93ffa --- /dev/null +++ b/windows/src/engine/kmrefresh/kmrefresh.vcxproj.filters @@ -0,0 +1,27 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;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 + + + + + Resource Files + + + \ No newline at end of file diff --git a/windows/src/engine/kmrefresh/version.in b/windows/src/engine/kmrefresh/version.in new file mode 100644 index 0000000000..de3e156673 --- /dev/null +++ b/windows/src/engine/kmrefresh/version.in @@ -0,0 +1,30 @@ +1 VERSIONINFO + FILEVERSION $VERSIONNUM + PRODUCTVERSION $VERSIONNUM + FILEFLAGSMASK 0x3fL + FILEFLAGS 0x0L + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "0C0904E4" + BEGIN + VALUE "CompanyName", "SIL International\0" + VALUE "FileDescription", "Keyman Engine x64\0" + VALUE "FileVersion", "$VERSION\0" + VALUE "InternalName", "KEYMANX64\0" + VALUE "LegalCopyright", "© SIL International\0" + VALUE "LegalTrademarks", "\0" + VALUE "OriginalFilename", "KEYMANX64.EXE\0" + VALUE "ProductName", "Keyman Engine\0" + VALUE "ProductVersion", "$VERSION\0" + VALUE "Comments", "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0xc09, 1252 + END + END diff --git a/windows/src/global/delphi/general/Keyman.System.Settings.pas b/windows/src/global/delphi/general/Keyman.System.Settings.pas index c8df2cdc03..6748477261 100644 --- a/windows/src/global/delphi/general/Keyman.System.Settings.pas +++ b/windows/src/global/delphi/general/Keyman.System.Settings.pas @@ -95,7 +95,7 @@ const ValueType: kstInteger ); - BaseKeymanSettings: array[0..32] of TKeymanSettingBase = ( + BaseKeymanSettings: array[0..33] of TKeymanSettingBase = ( // TIKE:UTikeDebugMode.TikeDebugMode ( @@ -423,6 +423,20 @@ const ValueType: kstInteger ), + // kmcomapi::TKeymanKeyboardsInstalled.TriggerWindowsLanguageSync + ( + ID: 'engine.compatibility.sync_languages_to_cloud'; + Name: SRegValue_Flag_SyncLanguagesToCloud; + RootKey: HKCU; + Key: SRegKey_KeymanEngineDebug_CU; + Description: 'Set to 0 to prevent Keyman from calling the experimental '+ + 'kmrefresh program that triggers Windows language '+ + 'synchronisation in Windows 10. This program ensures that '+ + 'language settings are not lost when you restart Windows.'; + DefaultInt: 1; + ValueType: kstInteger + ), + // // engine.diagnostics // diff --git a/windows/src/global/delphi/general/RegistryKeys.pas b/windows/src/global/delphi/general/RegistryKeys.pas index e6b46b4800..2c6f5d49d2 100644 --- a/windows/src/global/delphi/general/RegistryKeys.pas +++ b/windows/src/global/delphi/general/RegistryKeys.pas @@ -448,6 +448,7 @@ const SRegValue_Flag_UseRegisterHotkey = 'Flag_UseRegisterHotkey'; SRegValue_Flag_ShouldSerializeInput = 'Flag_ShouldSerializeInput'; SRegValue_Flag_UseAutoStartTask = 'Flag_UseAutoStartTask'; + SRegValue_Flag_SyncLanguagesToCloud = 'Flag_SyncLanguagesToCloud'; // Fixed path names const diff --git a/windows/src/global/delphi/general/utilexecute.pas b/windows/src/global/delphi/general/utilexecute.pas index 7dbce11379..b6f0c2c684 100644 --- a/windows/src/global/delphi/general/utilexecute.pas +++ b/windows/src/global/delphi/general/utilexecute.pas @@ -40,6 +40,8 @@ type class function CreateProcessAsShellUser(const process, cmdline: WideString; Wait: Boolean): Boolean; overload; class function CreateProcessAsShellUser(const process, cmdline: WideString; Wait: Boolean; var AExitCode: Cardinal): Boolean; overload; + + class function Execute(const cmdline, curdir: string; ShowWindow: Integer): Boolean; static; end; implementation @@ -256,6 +258,39 @@ begin end; end; +class function TUtilExecute.Execute(const cmdline, curdir: string; ShowWindow: Integer): Boolean; +var + si: TStartupInfoW; + pi: TProcessInformation; + buf: PChar; +begin + Result := False; + + si.cb := SizeOf(TStartupInfo); + si.lpReserved := nil; + si.lpDesktop := nil; + si.lpTitle := nil; + si.dwFlags := STARTF_USESHOWWINDOW; + si.wShowWindow := ShowWindow; + si.cbReserved2 := 0; + si.lpReserved2 := nil; + + buf := AllocMem((Length(cmdline)+1)*sizeof(Char)); + try + StrPCopy(buf, cmdline); + if CreateProcess(nil, buf, + nil, nil, True, NORMAL_PRIORITY_CLASS, nil, PWideChar(curdir), + si, pi) then + begin + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + Result := True; + end; + finally + FreeMem(buf); + end; +end; + // Refactored from UCreateProcessAsShellUser function GetShellWindow: HWND; stdcall; external user32;