fix(windows): set basekeyboard as current user not the admin user on a elevated process. - #16162
fix(windows): set basekeyboard as current user not the admin user on a elevated process.#16162rc-swag wants to merge 5 commits into
Conversation
User Test ResultsTest specification and instructions
Test Artifacts |
mcdurdin
left a comment
There was a problem hiding this comment.
I don't think this is correct. The base keyboard dialog needs to run elevated -- because when you change the base keyboard, Keyman needs to mcompile each of the installed keyboards against the new base keyboard:
keyman/windows/src/engine/kmcomapi/com/options/keymanoptions.pas
Lines 133 to 135 in 1536a5f
The problem here is that the base layout setting is saved against the Admin user, but needs to be saved against the current user. This probably is best solved by splitting the admin component -- mcompiling -- out of the TKeymanOptions.Apply function, and running it as a separate step from the Base Keyboard dialog. Then the Base Keyboard dialog does not show elevated, but just elevates when OK is clicked, if it detects that new mcompiles need to be run, and does that as a kmshell -mcompile <basekeylayoutid> call? (implementation calls: TKeymanKeyboardInstalled.UpdateBaseLayout for each installed keyboard).
TKPRecompileMnemonicKeyboard then needs a parameter for the base layout, rather than reading it from the context options:
So some plumbing required, sadly.
This change may not have been the prefered change but it worked. It does run as an elevated but the process is created as a current user. |
|
Did you verify that this worked with a baselayout you had never selected previously? How could the mcompiled .kmx files be written to the C:\ProgramData folder if the base layout steps are run non-elevated? One reasonably straightforward way to address this, for starting as a non-elevated user:
Also:
|
Need to verify that the relevant files are saved in ProgramData too and that the base keyboard is mapped as expected! |
Test Prerequisites
Test Specs
Test Results
Note The Keyman Configuration window becomes unclickable until I click out of the app and click back. It does not show unresponding.
|
Note, if a crash dialog appears, FAIL the test. Also, please Copy to Clipboard and paste it into the test report. TEST_BASE_KEYBOARD_CURRENT_USER (FAIL): a crash dialog appeared |
2d6a4ee to
2c48025
Compare
The original change, changed the InKeymanKyboardInstalled UpdateBaseLayout inteface and it didn't need to. This change restores it.
|
Test-bot: retest TEST_BASE_KEYBOARD_CURRENT_USER |
Test Specs
Test Results
|
There was a problem hiding this comment.
This is heading definitely in the right direction. But I am uncomfortable with the hackery we are having to do in order to get mcompile to run when we want it to -- especially the awareness that kmshell now has to have of specific registry values owned by kmcomapi, which is something we want to avoid.
So I think I'd like to change the way that kmcom.Options.Apply works here, removing its side-effect call to recompile the base layouts.
Instead, let's add a new API endpoint to run mcompile, and then have that called by kmshell.MCompileBaseKeyboard. We can do that as an endpoint on kmcomapi.IKeymanKeyboardInstalled I think, so then kmshell.MCompileBaseKeyboard becomes responsible for iterating over the installed keyboards and calling IKeymanKeyboardInstalled.RecompileBaseLayout?
It's a whole lot more explicit and much less magical than my original design (which was informed by a fairly torrid period in Windows COM design where side-effects were commonplace, but which has not held up over time). It also clarifies the elevation boundary condition much more neatly.
We will need to documented in Keyman Engine for Windows as a BREAKING CHANGE but it's pretty minimal impact. (Will need to document the change in the API docs around the koBaseLayout option.)
Similarly, the keyboard install process should continue to have the call to recompile base layouts so kmshell will need to pass the base layout to the elevated call to install the keyboard also.
(If you haven't previously edited the COM API, I can walk you through it; there are a few stumbling points but it's not awful)
|
|
||
| if FOldBaseLayout <> Get_Items('koBaseLayout').Value then | ||
| FNewBaseLayout := Get_Items('koBaseLayout').Value; | ||
| if IsAdministrator and (FOldBaseLayout <> FNewBaseLayout) then |
There was a problem hiding this comment.
Why are we testing for IsAdministrator here?
| if ki.MnemonicLayout then // I4169 | ||
| begin | ||
| with Context as TKeymanContext do | ||
| BaseKeyboardID := (Options as IKeymanOptions).Items['koBaseLayout'].Value; |
There was a problem hiding this comment.
will this still get the admin value of base layout? do we need to be able to pass baselayout as a param to elevated kmshell?
| begin | ||
| if FRegKeyboard.MnemonicLayout and FileExists(FRegKeyboard.KeymanFile) then // I4615 | ||
| begin | ||
| BaseKeyboardID := (Context.Options as IKeymanOptions).Items['koBaseLayout'].Value; |
There was a problem hiding this comment.
ditto, this will still get admin's baselayout option value
| begin | ||
| with TfrmBaseKeyboard.Create(nil) do | ||
| function ConfigureBaseKeyboard(out BaseKeyboardID: Integer): Boolean; | ||
| begin with TfrmBaseKeyboard.Create(nil) do |
There was a problem hiding this comment.
| begin with TfrmBaseKeyboard.Create(nil) do | |
| begin | |
| with TfrmBaseKeyboard.Create(nil) do |
| if BaseKeyboardNeedsMCompile(BaseKeyboardID) and not kmcom.SystemInfo.IsAdministrator then | ||
| MCompileResult := WaitForElevatedConfiguration(WindowHandle, '-mcompile ' + IntToHex(BaseKeyboardID, 8)) = 0; |
There was a problem hiding this comment.
This means if isadmin then mcompile will never be called? Don't we need:
| if BaseKeyboardNeedsMCompile(BaseKeyboardID) and not kmcom.SystemInfo.IsAdministrator then | |
| MCompileResult := WaitForElevatedConfiguration(WindowHandle, '-mcompile ' + IntToHex(BaseKeyboardID, 8)) = 0; | |
| if BaseKeyboardNeedsMCompile(BaseKeyboardID) then | |
| begin | |
| if not kmcom.SystemInfo.IsAdministrator then | |
| MCompileResult := WaitForElevatedConfiguration(WindowHandle, '-mcompile ' + IntToHex(BaseKeyboardID, 8)) = 0; | |
| else | |
| MCompileResult := ... call directly? | |
| end; |
| // This is hacky, maybe just remove the registry value, however that | ||
| // would not force a recompile if the was the default base layout. | ||
| // Options.Apply re-compiles only when it observes a changed base layout. | ||
| // The caller may be repairing missing files for the already-selected layout. | ||
| Reg := TRegistryErrorControlled.Create; | ||
| try | ||
| if Reg.OpenKey(SRegKey_KeymanEngine_CU, True) then | ||
| Reg.WriteString(SRegValue_UnderlyingLayout, '00000000'); | ||
| finally | ||
| Reg.Free; | ||
| end; |
| kmcom.Options['koBaseLayout'].Value := BaseKeyboardID; | ||
| try | ||
| if PreviousBaseKeyboardID = BaseKeyboardID then | ||
| ForceBaseLayoutChange; | ||
| kmcom.Options.Apply; | ||
| Result := True; | ||
| finally | ||
| kmcom.Options['koBaseLayout'].Value := PreviousBaseKeyboardID; | ||
| RestorePreviousBaseKeyboardValue; | ||
| end; |
There was a problem hiding this comment.
We can update the kmcom API and I think that might be better than side-effecting our way to a solution?
| ModalResult := mrOk; | ||
| end; | ||
|
|
||
| function MCompileBaseKeyboard(const BaseKeyboardIDText: string): Boolean; |
There was a problem hiding this comment.
I think we should move this into its own unit
|
Notes on COM API:
Forward declaration, Line 70 or close by: interface IKeymanKeyboardInstalled2;and declaration, define the interface (I generated a new GUID already: '{3086C85C-932A-4726-BF76-2D74DD133AC9}', can go near bottom of file: [
uuid(3086C85C-932A-4726-BF76-2D74DD133AC9),
version(19.0),
helpstring("https://help.keyman.com/developer/engine/windows/19.0/api/IKeymanKeyboardInstalled2"),
dual,
oleautomation
]
interface IKeymanKeyboardInstalled2: IKeymanKeyboardInstalled
{
[id(0x00000120)]
HRESULT _stdcall SetBaseKeyboard(long KLID);
};Then in the implementation, we need in keymanapi_TLB.pas: // *********************************************************************//
// Interface: IKeymanKeyboardInstalled2
// Flags: (4416) Dual OleAutomation Dispatchable
// GUID: {3086C85C-932A-4726-BF76-2D74DD133AC9}
// *********************************************************************//
IKeymanKeyboardInstalled2 = interface(IKeymanKeyboardInstalled)
['{3086C85C-932A-4726-BF76-2D74DD133AC9}']
procedure SetBaseKeyboard(KLID: Integer); safecall;
end;And finally, in keymankeyboardinstalled.pas: TKeymanKeyboardInstalled = class( // I3581
TKeymanKeyboard,
IIntKeymanKeyboardInstalled,
IKeymanKeyboardInstalled2)
...
protected
procedure SetBaseKeyboard(KLID: Integer); safecall;
end;and the implementation. Details on names, etc, up to you, but suggest using In usage in kmshell, you'll need to: (kbd as IKeymanKeyboardInstalled2).SetBaseKeyboard(klid); |
…oard-user-not-admin
# Keyman Conventional Commit suggestions:
#
# - Link to a Sentry issue with git trailer:
# Fixes: _MODULE_-_ID_
# - Give credit to co-authors:
# Co-authored-by: _Name_ <_email_>
# - Use imperative, present tense ('attach' not 'attaches', 'attached' etc)
# - Don't include a period at the end of the title
# - Always include a blank line before trailers
# - More: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes

Fixes: #15152
This was quicker fix then I thought the more through issue 15154 still remains to catch discover all the cases the non-admin user configuration changes that may been attempting.
Build-bot: release:windows
User Testing
TEST_BASE_KEYBOARD_CURRENT_USER
Login into Windows with and account that is a "standard" user and does not have "Administrator" rights.
Install the Keyman from this PR
Open Keyman Configuration -> Keyboard Layouts
Install a keyboard for example sil_ipa
Open Keyman Configuration -> Options
Check
C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipadon't delete the basesil_ipa.kmxbut delete any with KLIDs for examplesil_ipa-00000409-d.kmxClick Base Keyboard, change the Base Keyboard to German. You will need to enter the login details for a Admin user.
Confirm the Keyboard changes for the current user and not the Admin user used for the elevated processs.
Check
C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipathere should now be asil_ipa-????0407-d.kmxandsil_ipa-????0407.kmxTEST_BASE_KEYBOARD_CURRENT_USER_KMX_EXISTS
After completing the steps in
TEST_BASE_KEYBOARD_CURRENT_USERCheck German mcomplied kmx is still there i.e.
C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipathere should now be asil_ipa-????0407-d.kmxandsil_ipa-????0407.kmx6.Click Base Keyboard, change the Base Keyboard to German. You should Not be asked to enter a admin user.