Skip to content

fix(windows): set basekeyboard as current user not the admin user on a elevated process. - #16162

Draft
rc-swag wants to merge 5 commits into
masterfrom
fix/windows/15152/basekeyboard-user-not-admin
Draft

fix(windows): set basekeyboard as current user not the admin user on a elevated process.#16162
rc-swag wants to merge 5 commits into
masterfrom
fix/windows/15152/basekeyboard-user-not-admin

Conversation

@rc-swag

@rc-swag rc-swag commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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

  1. Login into Windows with and account that is a "standard" user and does not have "Administrator" rights.

  2. Install the Keyman from this PR

  3. Open Keyman Configuration -> Keyboard Layouts

  4. Install a keyboard for example sil_ipa

  5. Open Keyman Configuration -> Options

Check C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipa don't delete the base sil_ipa.kmx but delete any with KLIDs for example sil_ipa-00000409-d.kmx

  1. Click Base Keyboard, change the Base Keyboard to German. You will need to enter the login details for a Admin user.

  2. Confirm the Keyboard changes for the current user and not the Admin user used for the elevated processs.

  3. Check C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipa there should now be a sil_ipa-????0407-d.kmx and sil_ipa-????0407.kmx

TEST_BASE_KEYBOARD_CURRENT_USER_KMX_EXISTS

After completing the steps in TEST_BASE_KEYBOARD_CURRENT_USER

  1. Open Keyman Configuration -> Options
  2. Click Base Keyboard, change the Base Keyboard to English. You may need to enter the login details for a Admin user.
  3. Close Configuration
    Check German mcomplied kmx is still there i.e. C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipa there should now be a sil_ipa-????0407-d.kmx and sil_ipa-????0407.kmx
  4. Open Keyman Configuration -> Options
    6.Click Base Keyboard, change the Base Keyboard to German. You should Not be asked to enter a admin user.

@rc-swag rc-swag self-assigned this Jun 30, 2026
@github-project-automation github-project-automation Bot moved this to Todo in Keyman Jun 30, 2026
@keymanapp-test-bot keymanapp-test-bot Bot added the user-test-missing User tests have not yet been defined for the PR label Jun 30, 2026
@keymanapp-test-bot

keymanapp-test-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

User Test Results

Test specification and instructions

  • TEST_BASE_KEYBOARD_CURRENT_USER (PASSED) (notes)
  • TEST_BASE_KEYBOARD_CURRENT_USER_KMX_EXISTS (PASSED) (notes)

Test Artifacts

@mcdurdin mcdurdin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

if FOldBaseLayout <> Get_Items('koBaseLayout').Value then
for I := 0 to Context.Keyboards.Count - 1 do // I4169
(Context.Keyboards.Items[I] as IIntKeymanKeyboardInstalled).UpdateBaseLayout;

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:

with Context as TKeymanContext do
BaseKeyboardID := (Options as IKeymanOptions).Items['koBaseLayout'].Value;

So some plumbing required, sadly.

@keyman-server keyman-server modified the milestones: A19S32, A19S33 Jul 4, 2026
@rc-swag

rc-swag commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

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:

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.
I tested it last week and it changed the base keyboard for the non admin user. Not the admin. I will look into it again.

@keymanapp-test-bot keymanapp-test-bot Bot added user-test-required User tests have not been completed and removed user-test-missing User tests have not yet been defined for the PR labels Jul 6, 2026
@mcdurdin

mcdurdin commented Jul 6, 2026

Copy link
Copy Markdown
Member

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:

  1. CU: The Base Keyboard dialog itself should not be elevated - launch from the current process.
  2. CU: After clicking OK, verify if any mcompile is needed by looking to see if the needed .kmx files are already present in ProgramData.
  3. CU: If mcompile is required, then wait for elevated kmshell -mcompile <baseklid> should be called to run the mcompile process.
  4. LM: kmshell -mcompile will need to temporarily set the option[koBaseLayout] for the admin user in order to run the process. This will cause mcompile to be run. Reset option afterwards.
  5. CU: After the wait for elevated process step completes, set option[koBaseLayout] for current user.

Also:

  • If Keyman Configuration is started as an elevated user, skip steps 3 and 4.
  • Need to update kmcomapi to only run mcompile if elevated

@mcdurdin

mcdurdin commented Jul 6, 2026

Copy link
Copy Markdown
Member

5. Confirm the Keyboard changes for the current user and not the Admin user used for the elevated processs

Need to verify that the relevant files are saved in ProgramData too and that the base keyboard is mapped as expected!

@Meng-Heng Meng-Heng assigned Meng-Heng and unassigned Meng-Heng Jul 9, 2026
@Meng-Heng

Meng-Heng commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Test Prerequisites

  1. Setup a non-admin local user by following this https://www.wikihow.com/Create-a-New-Local-User-Account-in-Windows-10

Test Specs

  1. Windows 10 Bootcamp

Test Results

  • TEST_BASE_KEYBOARD_CURRENT_USER (PASSED):
  1. Launch Keyman version 19.0.249
  2. Open Keyman Configuration -> Options
  3. Click Base Keyboard -> Enter Admin's password -> Change the Base Keyboard to English - Irish

Note

The Keyman Configuration window becomes unclickable until I click out of the app and click back. It does not show unresponding.

  1. Confirmed: the Keyboard changes for the current user and not the Admin user.
  2. To clarify, log into Admin account
  3. Verified: The Base keyboard does not change to English - Irish.

Note

The 2nd and later attempts of changing the Base keyboard, this error pop up
image
The Base keyboard does not change until I click X, and then open Keyman again.

@keymanapp-test-bot keymanapp-test-bot Bot removed the user-test-required User tests have not been completed label Jul 9, 2026
@mcdurdin

mcdurdin commented Jul 9, 2026

Copy link
Copy Markdown
Member

The 2nd and later attempts of changing the Base keyboard, this error pop up

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

@keyman-server keyman-server modified the milestones: A19S33, A19S34 Jul 20, 2026
@rc-swag rc-swag closed this Jul 24, 2026
@rc-swag
rc-swag force-pushed the fix/windows/15152/basekeyboard-user-not-admin branch from 2d6a4ee to 2c48025 Compare July 24, 2026 02:03
@github-project-automation github-project-automation Bot moved this from Todo to Done in Keyman Jul 24, 2026
@rc-swag rc-swag reopened this Jul 24, 2026
@github-project-automation github-project-automation Bot moved this from Done to In Progress in Keyman Jul 24, 2026
The original change, changed the InKeymanKyboardInstalled
UpdateBaseLayout inteface and it didn't need to. This
change restores it.
@keyman-server keyman-server modified the milestones: A19S34, A19S35 Aug 3, 2026
@keymanapp-test-bot keymanapp-test-bot Bot added the user-test-required User tests have not been completed label Aug 4, 2026
@rc-swag

rc-swag commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Test-bot: retest TEST_BASE_KEYBOARD_CURRENT_USER

@Meng-Heng

Meng-Heng commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Test Specs

  1. Keyman for Windows v19.9.260-alpha-test-16162
  2. Windows 10 Bootcamp

Test Results

  • TEST_BASE_KEYBOARD_CURRENT_USER (PASSED):
  1. Login into Windows with and account that is a "standard" user
  2. Install the Keyman from this PR
  3. Dialog pops up asking for the Admin Authentication
  4. Open Keyman Configuration -> Keyboard Layouts
  5. Install a sil_ipa keyboard
  6. Open Keyman Configuration -> Options
  7. Go to C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipa
  8. Delete sil_ipa-00000409-d.kmx and sil_ipa-00000409.kmx both time asked for Admin login details
  9. Click Base Keyboard -> change the Base Keyboard to German -> provide Admin login details
  10. VERIFIED: the Keyboard changes for the current user and not the Admin user used for the elevated process
  11. Check C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipa
  12. VERIFIED: There are sil_ipa-00000407-d.kmx and sil_ipa-00000407.kmx` files.
  • TEST_BASE_KEYBOARD_CURRENT_USER_KMX_EXISTS (PASSED):
  1. Continue after testing TEST_BASE_KEYBOARD_CURRENT_USER
  2. Open Keyman Configuration -> Options
  3. Click Base Keyboard -> change the Base Keyboard to English -> Enter login details for a Admin user
  4. Check C:\ProgramData\Keyman\Keyman Engine\Keyboard\_Package\sil_ipa folder
  5. VERIFIED: sil_ipa-00000407-d.kmx and sil_ipa-00000407.kmx are still in the folder
  6. Return to Keyman Configuration -> Options
  7. Click Base Keyboard -> change the Base Keyboard to German
  8. VERIFIED: Changing the Base Keyboard back to German does not need to enter Admin details.

@keymanapp-test-bot keymanapp-test-bot Bot removed the user-test-required User tests have not been completed label Aug 6, 2026
@rc-swag
rc-swag requested a review from mcdurdin August 12, 2026 06:06

@mcdurdin mcdurdin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
begin with TfrmBaseKeyboard.Create(nil) do
begin
with TfrmBaseKeyboard.Create(nil) do

Comment on lines +183 to +184
if BaseKeyboardNeedsMCompile(BaseKeyboardID) and not kmcom.SystemInfo.IsAdministrator then
MCompileResult := WaitForElevatedConfiguration(WindowHandle, '-mcompile ' + IntToHex(BaseKeyboardID, 8)) = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means if isadmin then mcompile will never be called? Don't we need:

Suggested change
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;

Comment on lines +124 to +134
// 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not comfortable with this

Comment on lines +145 to +154
kmcom.Options['koBaseLayout'].Value := BaseKeyboardID;
try
if PreviousBaseKeyboardID = BaseKeyboardID then
ForceBaseLayoutChange;
kmcom.Options.Apply;
Result := True;
finally
kmcom.Options['koBaseLayout'].Value := PreviousBaseKeyboardID;
RestorePreviousBaseKeyboardValue;
end;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move this into its own unit

@mcdurdin

Copy link
Copy Markdown
Member

Notes on COM API:

  1. Establish a new interface in kmcomapi.ridl:

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 IKeymanKeyboardInstalled2 to match patterns that we have elsewhere.

In usage in kmshell, you'll need to:

(kbd as IKeymanKeyboardInstalled2).SetBaseKeyboard(klid);

@keyman-server keyman-server modified the milestones: A19S35, A19S36 Aug 17, 2026
@rc-swag
rc-swag marked this pull request as draft August 18, 2026 06:06
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

bug(windows): base keyboard user setting not set when using the current user is not an administrator

4 participants