Merge pull request #5002 from keymanapp/fix/windows/4976-redesign-keymanx64-lifecycle

fix(windows): make keymanx64 responsible for its own lifecycle
This commit is contained in:
Marc Durdin 2021-05-10 19:46:12 +10:00 committed by GitHub
commit a6a658e8bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 134 deletions

View file

@ -253,7 +253,6 @@ type
procedure WMUserStart(var Message: TMessage); message WM_USER_Start;
procedure WMUserParameterPass(var Message: TMessage); message WM_USER_ParameterPass;
procedure WMUserSendFontChange(var Message: TMessage); message WM_USER_SendFontChange;
procedure WMUserPlatformComms(var Message: TMessage); message WM_USER_PlatformComms;
procedure WMUserVisualKeyboardClosed(var Message: TMessage); message WM_USER_VisualKeyboardClosed; // I4243
procedure SetTrayIcon(rp: TRunningProduct; kbd: IKeymanKeyboardInstalled);
procedure TrayIconMouseUp(Sender: TObject; Button: TMouseButton;
@ -272,9 +271,7 @@ type
//procedure SnapToolHelp;
procedure RecreateTaskbarIcons;
function StartKeymanEngine: Boolean;
procedure SendPlatformComms64(msg, value: DWORD);
procedure ClosePlatformComms64;
procedure StartPlatformComms64;
procedure StartKeymanX64;
procedure OpenTextEditor;
//function GetCachedKeymanID(hkl: DWORD): DWORD;
procedure ShowLanguageSwitchForm;
@ -379,10 +376,6 @@ const
const
MSGFLT_ADD = 1;
const
PC_CLOSE = 2;
PC_GETAPPLICATION = 4;
const
KEYMANID_NONKEYMAN = -1;
KEYMANID_INVALID: Cardinal = $FFFFFFFD;
@ -555,8 +548,6 @@ begin
FreeAndNil(FInputPane);
ClosePlatformComms64;
UnregisterHotkeys;
with TRegistryErrorControlled.Create do // I2890
@ -1289,14 +1280,6 @@ begin
end;
end;
procedure TfrmKeyman7Main.WMUserPlatformComms(var Message: TMessage);
begin
case Message.wParam of
PC_GETAPPLICATION:
Message.Result := Application.Handle; // I3758
end;
end;
procedure TfrmKeyman7Main.WMUserSendFontChange(var Message: TMessage);
begin
PostMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
@ -1368,7 +1351,7 @@ begin
kmint.KeymanEngineControl.RestartEngine; // I1486
StartPlatformComms64;
StartKeymanX64;
RegisterHotkeys;
end;
@ -1969,25 +1952,6 @@ begin
FInUpdateOSKVisibility := False;
end;
procedure TfrmKeyman7Main.SendPlatformComms64(msg, value: DWORD);
var
hwnd: THandle;
begin
// TODO: Test Wow64
KL.Log('SendPlatformComms64 ENTER: %d %d', [msg, value]);
hwnd := FindWindow('Keymanx64', nil);
if hwnd <> 0 then
PostMessage(hwnd, WM_USER_PlatformComms, msg, value);
KL.Log('SendPlatformComms64 EXIT');
end;
procedure TfrmKeyman7Main.ClosePlatformComms64;
begin
SendPlatformComms64(PC_CLOSE, 0);
end;
type
TLangSwitchRefreshWatcher = class(TThread)
private
@ -2013,28 +1977,46 @@ begin
FLangSwitchRefreshWatcher.Start;
end;
procedure TfrmKeyman7Main.StartPlatformComms64;
procedure TfrmKeyman7Main.StartKeymanX64;
var
dir, s: WideString;
sei: TShellExecuteInfoW;
cmd: string;
si: TStartupInfoW;
pi: TProcessInformation;
v: PWideChar;
processHandle: THandle;
begin
if not IsWow64 then Exit; // I4374
dir := ExtractFilePath(ParamStr(0));
s := dir + 'keymanx64.exe';
try
FillChar(si, sizeof(TStartupInfoW), 0);
FillChar(pi, sizeof(TProcessInformation), 0);
si.cb := sizeof(TStartupInfo);
si.dwFlags := STARTF_USESHOWWINDOW;
si.wShowWindow := SW_HIDE;
if not FileExists(s) then Exit; // Keyman x64 is not installed
FillChar(sei, SizeOf(sei), 0);
sei.cbSize := SizeOf(sei);
sei.Wnd := Handle;
sei.lpVerb := 'open';
sei.lpFile := PWideChar(s);
sei.lpParameters := '';
sei.lpDirectory := PWideChar(dir);
sei.nShow := SW_SHOW;
if not ShellExecuteExW(@sei) then Exit; // log
if not DuplicateHandle(GetCurrentProcess, GetCurrentProcess,
GetCurrentProcess, @processHandle, 0, True, DUPLICATE_SAME_ACCESS) then
RaiseLastOSError;
cmd := Format('%s%s %d %d %d', [ExtractFilePath(ParamStr(0)), 'keymanx64.exe', processHandle, Handle, Application.Handle]);
// Duplicate the string because CreateProcess requires a mutable buffer, so
// this guarantees it
v := StrNew(PWideChar(cmd));
try
if not CreateProcessW(nil, v, nil, nil, True, NORMAL_PRIORITY_CLASS, nil, nil, si, pi) then
RaiseLastOSError;
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
finally
StrDispose(v);
end;
except
on E:Exception do
begin
// We're going to handle any exceptions here but we'd like to know that
// they happened
TKeymanSentryClient.ReportHandledException(E, 'Error starting keymanx64', True);
end;
end;
end;
procedure TfrmKeyman7Main.HotkeyWndProc(var Message: TMessage);
@ -2217,5 +2199,4 @@ initialization
ChangeWindowMessageFilter(wm_keyman_globalswitch, MSGFLT_ADD);
ChangeWindowMessageFilter(wm_keyman_globalswitch_process, MSGFLT_ADD);
ChangeWindowMessageFilter(wm_keyman_control_internal, MSGFLT_ADD); // I3933
ChangeWindowMessageFilter(WM_USER_PlatformComms, MSGFLT_ADD);
end.

View file

@ -25,20 +25,13 @@
#include "keymansentry.h"
// Forward declarations of functions included in this code module
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL Fail(HWND, PWSTR);
BOOL UniqueInstance();
// Global constants
//#define KMC_GETLOADED 3
#define WM_USER_PlatformComm (WM_USER+103)
#define PC_CLOSE 2
#define PC_GETLOADED 3
#define PC_GETAPPLICATION 4 // I3758
BOOL ParseCmdLine(LPTSTR lpCmdLine);
BOOL CreateWatcherThread(HWND hWnd);
// External functions in Keyman64.dll
@ -54,8 +47,15 @@ extern "C" BOOL __declspec( dllimport ) WINAPI Keyman_UnregisterControllerWindow
// Global variables
HINSTANCE hInst; // current instance
HWND hwndController = NULL, hwndControllerOwner = NULL; // keyman x86 controller window handles // I3758
HINSTANCE hInst; // Current instance
HWND hwndController = NULL; // Keyman x86 Main Form window handle
HWND hwndControllerOwner = NULL; // Keyman x86 Application window handles
HANDLE hParentProcessHandle = NULL; // Keyman x86 process handle
HANDLE hWatcherThread = NULL;
HANDLE hWatcherThreadTerminateEvent = NULL;
BOOL KeymanStarted = FALSE;
// Global strings
@ -75,12 +75,13 @@ const PWSTR
szError_FailedToRegister = L"Failed to register window class",
szError_FailedToInitInstance = L"Failed to initialise application",
szError_InvalidCommandline = L"Incorrect command line for Keyman Engine x64; should be passed handle of Keyman Engine x86",
szError_WatcherThreadFailed = L"Failed to create watcher thread",
szFail_UnknownError = L"Unknown error %d",
szFail_ErrorFormat = L"%s: %s (%d)",
szFail_ErrorFormat_OtherUnknown = L"%s: An unknown error occurred",
szKeymanX64Mutex = L"KeymanEXEx6470",
szWindowClass_x86_Wnd = L"TfrmKeyman7Main"; // Do not localize
//const char *szGPA_ChangeWindowMessageFilter = "ChangeWindowMessageFilter"; // Do not localize
@ -116,11 +117,8 @@ int APIENTRY _tWinMain(HINSTANCE hInstance,
MSG msg;
if (!UniqueInstance()) {
// If keymanx64 is already running, let's not
// report an error. This can happen if keyman.exe
// fails unexpectedly, and is then restarted.
//Fail(0, szError_CannotRunMultipleInstances);
if (!ParseCmdLine(lpCmdLine)) {
Fail(0, szError_InvalidCommandline);
keyman_sentry_shutdown();
return 1;
}
@ -157,21 +155,34 @@ int APIENTRY _tWinMain(HINSTANCE hInstance,
return (int) msg.wParam;
}
BOOL UniqueInstance()
{
HANDLE hMutex = CreateMutex(NULL, FALSE, szKeymanX64Mutex);
if(!hMutex) return FALSE;
//
// FUNCTION: ParseCmdLine(LPSTSTR)
//
// PURPOSE: Parses command line and finds input parameters
//
// COMMENTS:
//
// Reads parent process handle, parent process main window and
// parent process application window handle from the command
// line and validates them.
//
BOOL ParseCmdLine(LPTSTR lpCmdLine) {
while (iswspace(*lpCmdLine)) lpCmdLine++;
if (!*lpCmdLine) return FALSE;
hParentProcessHandle = (HANDLE)wcstoull(lpCmdLine, &lpCmdLine, 10);
if (hParentProcessHandle == 0 || hParentProcessHandle == (HANDLE) ULLONG_MAX) return FALSE;
switch(WaitForSingleObject(hMutex, 0))
{
case WAIT_ABANDONED:
case WAIT_OBJECT_0:
return TRUE;
case WAIT_FAILED:
case WAIT_TIMEOUT:
return FALSE;
}
return FALSE;
while (iswspace(*lpCmdLine)) lpCmdLine++;
if (!*lpCmdLine) return FALSE;
hwndController = (HWND) wcstoull(lpCmdLine, &lpCmdLine, 10);
if (hwndController == 0 || hwndController == (HWND) ULLONG_MAX) return FALSE;
while (iswspace(*lpCmdLine)) lpCmdLine++;
if (!*lpCmdLine) return FALSE;
hwndControllerOwner = (HWND) wcstoull(lpCmdLine, &lpCmdLine, 10);
if (hwndControllerOwner == 0 || hwndControllerOwner == (HWND) ULLONG_MAX) return FALSE;
return TRUE;
}
//
@ -289,24 +300,11 @@ BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
//
BOOL StartKeyman(HWND hWnd)
{
if(!ChangeWindowMessageFilter(WM_USER_PlatformComm, MSGFLT_ADD))
return Fail(hWnd, szError_ChangeWindowMessageFilter);
if(!Keyman_ResetInitialisation()) // I3092
return Fail(hWnd, szError_FailedToInitialise);
hwndController = FindWindow(szWindowClass_x86_Wnd, NULL); // I3758
if(hwndController == NULL)
return Fail(hWnd, szError_Keymanx86NotFound);
DWORD_PTR dwResult;
if(SendMessageTimeout(hwndController, WM_USER_PlatformComm, PC_GETAPPLICATION, 0, SMTO_BLOCK, 5000, &dwResult) == 0) // I3758
return Fail(hWnd, szError_Keymanx86NotFound);
hwndControllerOwner = (HWND) dwResult; // I3758
if(hwndControllerOwner == NULL)
return Fail(hWnd, szError_Keymanx86NotFound);
if (!CreateWatcherThread(hWnd))
return Fail(hWnd, szError_WatcherThreadFailed);
if(!Keyman_RegisterControllerWindow(hwndControllerOwner) || !Keyman_RegisterControllerWindow(hwndController)) // I3758
return Fail(hWnd, szError_FailedToRegisterController);
@ -314,28 +312,56 @@ BOOL StartKeyman(HWND hWnd)
if(!Keyman_Initialise(hWnd, FALSE))
return Fail(hWnd, szError_FailedToInitialise);
KeymanStarted = TRUE;
return TRUE;
}
//
// FUNCTION: SendPlatformComms32(WPARAM, LPARAM)
// FUNCTION: WatcherThreadProc(LPVOID)
//
// PURPOSE: Post a command to Keyman Engine x86
// PURPOSE: Watches for parent process termination
//
BOOL SendPlatformComms32(WPARAM wParam, LPARAM lParam)
{
// Search again; lets us reconnect if
// Keyman.exe crashes and is retarted
HWND hwndLocalController = FindWindow(szWindowClass_x86_Wnd, NULL);
if(hwndLocalController == NULL)
{
// TODO: Perhaps we should trigger a shutdown here, because
// keyman.exe was not found. Right now, there is some confusion
// in regards to responsibility. See #4976
MessageBox(0, szError_Keymanx86NotFound_Comms, szTitle, MB_OK);
// COMMENTS:
//
// The watcher thread triggers destruction of this process
// as soon as the parent process is terminated.
//
DWORD WINAPI WatcherThreadProc(LPVOID lpParameter) {
HWND hwnd = (HWND)lpParameter;
HANDLE handles[2] = { hWatcherThreadTerminateEvent, hParentProcessHandle };
if (WaitForMultipleObjects(2, handles, FALSE, INFINITE) == WAIT_OBJECT_0 + 1) {
// We'll post a message to our window if the parent process terminated,
// but not during a shutdown in other circumstances, because that would
// have been triggered by a shutdown of the process anyway.
PostMessage(hwnd, WM_CLOSE, 0, 0);
}
return 0;
}
//
// FUNCTION: CreateWatcherThread(HWND)
//
// PURPOSE: Creates the Watcher Thread
//
// COMMENTS:
//
// Creates the watcher thread which watches for
// termination of the parent process.
//
BOOL CreateWatcherThread(HWND hWnd) {
hWatcherThreadTerminateEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (hWatcherThreadTerminateEvent == NULL)
return FALSE;
hWatcherThread = CreateThread(NULL, 0, WatcherThreadProc, hWnd, 0, NULL);
if (hWatcherThread == NULL) {
CloseHandle(hWatcherThreadTerminateEvent);
hWatcherThreadTerminateEvent = NULL;
return FALSE;
}
PostMessage(hwndLocalController, WM_USER_PlatformComm, wParam, lParam);
return TRUE;
}
@ -347,24 +373,40 @@ BOOL SendPlatformComms32(WPARAM wParam, LPARAM lParam)
//
void Shutdown()
{
int wm_keyman = RegisterWindowMessage(L"wm_keyman");
DWORD_PTR dwResult;
if (KeymanStarted) {
int wm_keyman = RegisterWindowMessage(L"wm_keyman");
DWORD_PTR dwResult;
Keyman_StartExit(); // I3092
Keyman_StartExit(); // I3092
/* Tell all threads that it is time to exit. This is important to do before we shutdown
because we have got a per-thread keyboard hook that needs to be detached before we
lose our message hooks. */
/* Tell all threads that it is time to exit. This is important to do before we shutdown
because we want to try and detach from as many processes as possible so we don't
remain locked in memory. */
SendMessageTimeout(HWND_BROADCAST, wm_keyman, KM_EXIT, 0, SMTO_NORMAL, 1000, &dwResult); // I3092
SendMessageTimeout(HWND_BROADCAST, wm_keyman, KM_EXIT, 0, SMTO_NORMAL, 1000, &dwResult); // I3092
Keyman_Exit();
Keyman_Exit();
}
/* Unregister those windows that we registered earlier - don't fail on error though */
if(hwndController != NULL) Keyman_UnregisterControllerWindow(hwndController); // I3758
if(hwndControllerOwner != NULL) Keyman_UnregisterControllerWindow(hwndControllerOwner); // I3758
PostQuitMessage(0);
/* Cleanup */
if (hWatcherThreadTerminateEvent != NULL) {
// Both of these handles will be valid if one of them is, per CreateWatcherThread
SetEvent(hWatcherThreadTerminateEvent);
WaitForSingleObject(hWatcherThread, INFINITE);
CloseHandle(hWatcherThread);
CloseHandle(hWatcherThreadTerminateEvent);
}
if (hParentProcessHandle != NULL) {
CloseHandle(hParentProcessHandle);
}
}
//
@ -374,7 +416,6 @@ void Shutdown()
//
// WM_CREATE - initialise
// WM_DESTROY - post a quit message and return
// WM_USER_PlatformComm - process commands from Keyman Engine x86
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
@ -389,12 +430,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
Shutdown();
break;
case WM_USER_PlatformComm:
switch(wParam)
{
case PC_CLOSE: PostMessage(hWnd, WM_CLOSE, 0, 0); break;
}
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}

View file

@ -31,7 +31,6 @@ const
WM_USER_Start = WM_USER+101;
WM_USER_ParameterPass = WM_USER+100;
WM_USER_SendFontChange = WM_USER+102;
WM_USER_PlatformComms = WM_USER+103;
WM_USER_VisualKeyboardClosed = WM_USER+105; // I4242
const