fix(windows-ime): keep the TSF text service always active instead of switching IMEs - #1143
Open
DepengWang wants to merge 1 commit into
Open
DepengWang wants to merge 1 commit into
DepengWang wants to merge 1 commit into
Conversation
In TSF mode every dictation switched the session's input profile to the OpenLess keyboard TIP and back. Switching back makes TSF re-activate the user's own IME on every TSF thread of every process. With Weasel (Rime) that re-activation does blocking named-pipe IPC ending in FlushFileBuffers, which sometimes never returns and freezes the host. Observed on Windows 11 with Weasel 0.17.4: explorer.exe AppHang shortly after a dictation, with the desktop thread stuck in explorer message loop -> msctf -> weasel.dll -> FlushFileBuffers while every WeaselServer worker sat idle in ReadFile. Stopping WeaselServer released the thread at once. This matches Open-Less#665 and Open-Less#954 (both Weasel users) and is also the source of the restore glitches in Open-Less#1033 / Open-Less#1032. Register the text service under GUID_TFCAT_TIP_SPEECH for all languages (0xFFFF), like the system speech text service. TSF keeps such a TIP active next to the keyboard IME, so nothing has to be switched: - DllRegisterServer registers the speech category and removes the keyboard category and zh-CN profile left by earlier releases, so an upgrade migrates in place. - The per-dictation capture / activate / restore of the input profile is removed (windows_ime_restore.rs, most of windows_ime_profile.rs, the prepared-session plumbing in core_adapters.rs). A dictation is now a single submit to the IME window in the target app. - A speech profile is only activated while enabled, so the "show in keyboard list" preference no longer disables it. The text service no longer appears in the keyboard list at all; the preference is now inert on Windows and left in place for a follow-up decision. - Registration checks, the install smoke script and the settings blurb follow the new registration. Because the DLL is now active in every TSF-enabled process all the time, it also stops running a worker thread and a named pipe per TSF thread: - OpenLess sends WM_COPYDATA (token + UTF-16 text) to the message-only window the DLL already owned on the TSF thread. The DLL acknowledges, posts a message to itself and commits from the top of the host message loop, as before; the result is polled with the same token, which also covers hosts that only grant an async edit session (Word). - Activate()/Deactivate() no longer start, signal or join anything. Deactivate() used to join the worker on the host UI thread, which is unbounded if it runs with the loader lock held. - The window does not opt in to WM_COPYDATA from lower-integrity senders, so a non-elevated process still cannot inject text into an elevated host. - Timeout semantics are unchanged: anything after the submit was delivered stays OutcomeUnknown and never triggers a second insertion. The lifecycle contract test now asserts that the DLL contains no threads, pipes or blocking waits and that the protocol constants match between C++ and Rust. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refs #954, #665, #1033, #1032.
In TSF insertion mode, every dictation switched the session's input profile to the OpenLess keyboard TIP and back. Switching back makes TSF re-activate the user's own IME on every TSF thread of every process. With Weasel (Rime) that re-activation does blocking named-pipe IPC and can hang the host forever. That is the explorer/taskbar AppHang reported in #665 and #954.
This PR registers the OpenLess text service under the TSF speech category instead of the keyboard category. TSF keeps a speech TIP active next to the keyboard IME, so dictation no longer switches anything: it just submits text to the text service that is already active in the target app.
Root cause (dump-verified)
Windows 11, Weasel 0.17.4,
windowsInsertionMode=tsf. explorer.exe hung 6 times in two days, starting the day TSF mode was first used; no explorer hangs in the three weeks before.explorer message loop -> msctf -> weasel.dll -> KERNELBASE!FlushFileBuffers -> NtFlushBuffersFile.A second dump four minutes later shows the same thread at the same stack pointer.
ReadFile, one inConnectNamedPipe. The server is not stuck; client and server are out of sync on that pipe.Deactivate(); the next thing TSF does on that thread is re-activate Weasel.The blocking
FlushFileBuffersis Weasel's bug, but OpenLess is what triggers it on every dictation, and any IME with a slow or fragile activation path is exposed the same way.Changes
Always active instead of switching
DllRegisterServerregistersGUID_TFCAT_TIP_SPEECHfor all languages (0xFFFF, like the systemSpTip.dll) and removes the keyboard category and zh-CN profile from earlier releases, so upgrading migrates in place.windows_ime_restore.rs, most ofwindows_ime_profile.rs, and the prepared-session plumbing incore_adapters.rs. A dictation is now a single submit.No thread or pipe inside host processes
The DLL is now active in every TSF-enabled process all the time, so it should be as passive as possible there.
Deactivate()no longer joins a thread on the host UI thread (unbounded if it runs under the loader lock).WM_COPYDATA(token + UTF-16 text) to the message-only window the DLL already had. The DLL acknowledges, posts to itself and commits from the top of the host message loop as before; the result is polled with the same token, which also covers async edit sessions (Word).WM_COPYDATAfrom lower-integrity senders.OutcomeUnknownand never re-inserts. A mismatched app/DLL pair fails before dispatch and uses the non-TSF fallback.Compatibility
windowsShowOpenlessInKeyboardListpreference is now inert on Windows. I left the setting and its UI in place; removing or repurposing it is a product decision.Testing
On Windows 11 with Weasel as the keyboard IME:
weasel.dllstill loaded in each.cargo check --lockedon stable and on 1.88.0,rustfmt --checkon the touched files, prettier on the touched files,tsc && vite build.scripts/*.test.mjs: 53 of 56 pass. The 3 failures (android-apk-workflow-contract,ci-cache-usage,ci-changed-areas) fail identically on an untouchedupstream/betacheckout on this machine.windows_ime_ipc.rs/windows_ime_protocol.rsand an end-to-end round trip against the TSF-activated DLL, run from a small standalone harness.Not tested:
cargo test --libfor the app itself: on this machine the test binary exits at load with0xC0000139, before any test runs and regardless of this change. The tests inwindows_ime_session.rs/windows_ime_profile.rshave therefore only been compiled here.regsvr32on the built DLLs.🤖 Generated with Claude Code