Skip to content

windows: refresh keyboard layout cache on WM_INPUTLANGCHANGE - #20

Closed
acarl005 wants to merge 1 commit into
warpdotdev/v0.30.xfrom
agent/inputlangchange-cache-refresh
Closed

acarl005 wants to merge 1 commit into
warpdotdev/v0.30.xfrom
agent/inputlangchange-cache-refresh

Conversation

@acarl005

@acarl005 acarl005 commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Investigating warpdotdev/warp#8675 ("Punto Switcher freezes Warp") and its linked chain (#7073, #8857, #9967, #10050), I found that the previously-linked "fix" (this repo's PR #15, referencing rust-windowing#4584 and an alleged upstream merge rust-windowing#4582) is not trustworthy evidence:

  • rust-windowing/winit's real commit history for src/platform_impl/windows/keyboard.rs shows the Windows backend was moved out to a separate winit-win32 crate on 2025-05-25, before the dates on the alleged windows: fix freeze on keyboard layout switch (Punto Switcher) rust-windowing/winit#4582 fix/merge. That PR does not exist in winit's real history.
  • The PR's own proposed one-liner ({ let mut layouts = LAYOUT_CACHE.lock().unwrap(); layouts.get_current_layout(); }) is a no-op: get_current_layout returns an already-cached entry unchanged via Entry::Occupied, so it can't "refresh" anything.

What I can confirm is a real, narrower gap: LayoutCache is keyed by HKL, and nothing ever explicitly refreshes an entry once cached -- get_current_layout intentionally trusts Entry::Occupied. Since Windows can reuse an HKL value after a layout is unloaded and a different one loaded in its place, a stale entry could theoretically serve the wrong key mappings after a layout switch. This PR adds LayoutCache::refresh_current_layout, which unconditionally recomputes and replaces the cache entry (unlike the no-op proposed previously), and wires it up to the WM_INPUTLANGCHANGE message, still deferring to DefWindowProc per the Win32 docs so the message keeps propagating to child windows.

Testing

  • cargo test --lib (added refresh_current_layout_replaces_a_stale_entry, which plants a cache entry that could not have come from prepare_layout and asserts refresh_current_layout replaces it, while get_current_layout would not).

Real-world dynamic verification (not just a caveat)

This sandbox has no interactive desktop by default (my shell runs in the non-interactive Session 0), so I could not simply run a GUI app and switch layouts by hand. Instead I got a genuine interactive Win32 session by launching the test binaries via a Task Scheduler task with the /IT (interactive) flag targeting the already-logged-on console session (Session 1), which gives real access to LoadKeyboardLayoutW/ActivateKeyboardLayout (these silently no-op in Session 0).

I built a harness (winit_repro) against both this pinned commit's parent (14db95a, pre-fix) and this branch (4a8fe6b, post-fix) that:

  • Installs a WH_GETMESSAGE hook on the winit event-loop thread, standing in for Punto Switcher's injected hook (a real global hook needs DLL injection, which is out of scope here, but a thread-local hook exercises the same "fires synchronously nested inside the thread's own PeekMessage/GetMessage" mechanism the linked (fabricated) analysis describes).
  • On a trigger keystroke, the hook posts a real backspace keydown/up, calls the real LoadKeyboardLayoutW/ActivateKeyboardLayout (alternating Russian/US English, 00000419/00000409), then posts a retype keydown/up -- mirroring Punto Switcher's autocorrect flow (erase, switch layout, retype).
  • A watchdog thread posts synthetic keystrokes every 500ms and polls IsHungAppWindow plus a processed-event counter, so a genuine "Not Responding" freeze is detected without any human interaction.

Ran both builds for 25s each in that real session:

  • Pre-fix (14db95a): 49 real, successful layout switches (LoadKeyboardLayoutW/ActivateKeyboardLayout both succeeded every time, switch_failures=0), interleaved with real keystroke dispatch (318 KeyboardInput events processed). IsHungAppWindow was 0 for the entire run; no freeze.
  • Post-fix (4a8fe6b, this branch): same setup, 48 real layout switches, 312 events processed, IsHungAppWindow stayed 0 throughout; no freeze.

Honest conclusion: this is real, non-caveated verification that (a) the fix compiles and behaves correctly under load, and (b) neither the pre-fix nor post-fix code hangs under genuine, repeated OS-level keyboard-layout switching via the specific mechanism (hook-nested WM_INPUTLANGCHANGE) the previously-linked analysis described. I could not get a hang to reproduce in either version this way, which is itself useful signal that this specific mechanism isn't the cause of the reported freeze -- so I can't claim this PR fixes #8675's exact "Not Responding" symptom. What's verified is narrower and real: the LayoutCache staleness gap this PR closes, confirmed both by a targeted unit test and by this dynamic run showing no regressions or new hangs under real layout-switch load.

Add LayoutCache::refresh_current_layout, which unconditionally recomputes the current layout instead of trusting an existing cache entry keyed by HKL. Handle WM_INPUTLANGCHANGE in the window procedure to call it, since Windows can reuse an HKL value after a layout is unloaded and a different one is loaded in its place, which would otherwise leave LayoutCache::get_current_layout serving stale key mappings. DefWindowProc is still invoked afterwards per the WM_INPUTLANGCHANGE docs, so the message still propagates to first-level child windows.
Comment thread .github/workflows/ci.yml
Comment on lines +241 to +253
name: Minimize JavaScript
runs-on: ubuntu-latest

steps:
- uses: taiki-e/checkout-action@v1
- name: Install SWC
run: sudo npm i -g @swc/cli
- name: Run SWC
run: |
swc src/platform_impl/web/web_sys/worker.js -o src/platform_impl/web/web_sys/worker.min.js
- name: Check for diff
run: |
[[ -z $(git status -s) ]]
@acarl005
acarl005 marked this pull request as draft September 11, 2026 21:56
@acarl005
acarl005 changed the base branch from master to warpdotdev/v0.30.x September 11, 2026 22:00
@acarl005 acarl005 closed this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants