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;