fix(WeaselIPC, WeaselServer): 客戶端 IPC 改為有界等待,托盤刷新移至專用執行緒(#1909 後續) - #1914
Open
Wujidadi wants to merge 5 commits into
Open
fix(WeaselIPC, WeaselServer): 客戶端 IPC 改為有界等待,托盤刷新移至專用執行緒(#1909 後續)#1914Wujidadi wants to merge 5 commits into
Wujidadi wants to merge 5 commits into
Conversation
TSF 模組載入於每個文字輸入行程,焦點切換時會在宿主程式的 UI 執行緒上與 WeaselServer 同步往返。 `_Connect()` 的無上限重試迴圈與 `_Receive()`/`_WritePipe()` 的無逾時阻塞讀寫, 會使伺服器一忙碌,便將整個視窗凍結數秒,表現為視窗變灰、無回應,且跨 VS Code、檔案總管、Windows Terminal 一致,Alt+Tab 不受影響。 改動均收斂於 **WeaselIPC**,伺服器端呼叫以 `timeout_ms` 預設 `INFINITE` 維持原行為: - `_Connect()` 改為 fail-fast,移除 WaitNamedPipe 無限迴圈,一次連不上即拋出 `ERROR_PIPE_BUSY`,由既有 `_Ensure()` 接住。 - 管線改用 `FILE_FLAG_OVERLAPPED`,新增 `_WaitIo()` 對讀寫套用 `WaitForSingleObject` 逾時,逾時即 `CancelIoEx` 視同斷線,保留 `ERROR_MORE_DATA` 兩段式讀取語義。 - 移除 `_WritePipe()` 內的 `FlushFileBuffers`。 - `_ConnectServerPipe()` 配合改為 `OVERLAPPED`,並修掉失敗時的控制代碼洩漏。 客戶端逾時依指令性質(`_TimeoutForCommand`)分兩級: - 組字/按鍵路徑維持 `kClientIoTimeoutMs=500` 以確保按鍵不被誤放。 - 焦點/通知路徑(含 `OnSetThreadFocus` 的 keycode 0 焦點刷新探測)用 `kClientFocusTimeoutMs=25`,漏掉一次即自癒,讓視窗切換肉眼無感。 失敗經既有 `catch(DWORD)` 讓輸入法安靜降級(該次按鍵放行、下次自動重連),宿主程式不受影響。
`_Reconnect()` 會關閉原本的管線控制代碼並重新連線,但 `catch` 區塊的重送仍寫入按值傳入的舊控制代碼,該次重送必定失敗。 改為自 `_GetPipeHandle()` 取重連後的控制代碼重送。
rime#1912 把 `Shell_NotifyIcon` 從管線工作執行緒移到伺服器訊息執行緒,但訊息執行緒同時擁有候選視窗。 管線工作執行緒在 `FocusIn` → `_UpdateUI()` 仍會對候選視窗做跨執行緒的 ShowWindow/SetWindowPos,這些呼叫會同步送訊息給訊息執行緒; 而 `Shell_NotifyIcon` 內部以 `SMTO_BLOCK` 等待工作列,期間不處理送入的訊息。 當工作列 UI 執行緒本身正在等管線回應時,工作執行緒等待訊息執行緒、訊息執行緒等待工作列、工作列等待工作執行緒的環仍可能成立。 - WeaselTrayIcon 改由專用執行緒套用刷新:`RequestRefresh()` 只存快照並喚醒,`RefreshThreadProc()` 合併待處理請求後呼叫 `Refresh()`,`DisableRefresh()` 停止並 join。 - 移除 rime#1912 引入、現已無用的 `WM_WEASEL_SERVICE_NOTIFY`、`ServerImpl::OnServiceNotifyMessage` 與 `Server::SetTrayRefreshCallback`。 - 移除 `_RefreshTrayIcon()` 對 explorer.exe 延遲 100ms 另開執行緒的繞道:回呼已不再阻塞,對任何客戶端同步呼叫皆安全。
There was a problem hiding this comment.
Pull request overview
This PR follows up on #1909/#1912 by (1) making WeaselIPC client-side pipe operations bounded (so host-app UI threads don’t block indefinitely when WeaselServer is busy) and (2) moving tray refresh execution off the server message thread to further reduce deadlock risk involving Explorer/taskbar interactions.
Changes:
- Add client-side IPC timeouts via overlapped pipe I/O and per-command timeout selection (focus vs keystroke paths).
- Fix resend-after-reconnect to use the new pipe handle instead of the stale one.
- Move tray icon refresh to a dedicated thread and remove the prior server-message-thread refresh callback/WM message path and the explorer-specific delayed workaround.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| WeaselServer/WeaselTrayIcon.h | Introduces dedicated tray refresh thread plumbing (thread member, API changes). |
| WeaselServer/WeaselTrayIcon.cpp | Implements coalesced refresh processing on a dedicated thread and shutdown/join behavior. |
| WeaselServer/WeaselServerApp.cpp | Removes server refresh callback wiring; uses direct RequestRefresh() + DisableRefresh(). |
| WeaselIPCServer/WeaselServerImpl.h | Removes WM_WEASEL_SERVICE_NOTIFY handler and tray refresh callback plumbing. |
| WeaselIPCServer/WeaselServerImpl.cpp | Removes tray refresh message handler and Server::SetTrayRefreshCallback implementation. |
| WeaselIPC/WeaselClientImpl.cpp | Adds _TimeoutForCommand() and applies bounded timeouts to client Transact. |
| WeaselIPC/PipeChannel.cpp | Implements overlapped connect/read/write + bounded wait/cancel behavior and server-side overlapped ConnectNamedPipe. |
| RimeWithWeasel/RimeWithWeasel.cpp | Removes the explorer-specific delayed refresh workaround (now safe to call synchronously). |
| include/WeaselIPC.h | Removes WM_WEASEL_SERVICE_NOTIFY and Server::SetTrayRefreshCallback from the public interface. |
| include/PipeChannel.h | Adds timeout constants and timeout-capable Transact/_Send/_ReceiveResponse API surface. |
Suppressed comments (2)
WeaselIPC/PipeChannel.cpp:101
- _WaitIo can block indefinitely after a timeout/WAIT_FAILED because it unconditionally calls WaitForSingleObject(INFINITE) after CancelIoEx without checking whether CancelIoEx actually succeeded (or why it failed). If CancelIoEx fails unexpectedly, the intended bounded-wait behavior can be lost. Handle CancelIoEx failure explicitly and preserve the original wait error when throwing.
if (wait != WAIT_OBJECT_0) {
::CancelIoEx(pipe, &ov);
::WaitForSingleObject(ov.hEvent, INFINITE);
_ThrowCode(wait == WAIT_TIMEOUT ? static_cast<DWORD>(ERROR_TIMEOUT)
: ::GetLastError());
WeaselIPC/PipeChannel.cpp:177
- In _ConnectServerPipe, the overlapped ConnectNamedPipe wait ignores the return value of WaitForSingleObject. If the wait fails (e.g., invalid event handle), the code proceeds to GetOverlappedResult and can report a confusing error. Check the wait result and fail/close the pipe handle immediately on WAIT_FAILED.
if (!ok && err == ERROR_IO_PENDING) {
::WaitForSingleObject(op.ov.hEvent, INFINITE);
DWORD n = 0;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+19
to
+26
| struct OverlappedOp { | ||
| OVERLAPPED ov; | ||
| OverlappedOp() : ov() { ov.hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL); } | ||
| ~OverlappedOp() { | ||
| if (ov.hEvent) | ||
| ::CloseHandle(ov.hEvent); | ||
| } | ||
| }; |
| state = m_pending_state; | ||
| m_refresh_pending = false; | ||
| m_refresh_in_progress = true; | ||
| Refresh(state); |
- `OverlappedOp` 於 `CreateEvent` 失敗時立即拋出,避免後續以空事件等待。 - `_WaitIo()` 在 `CancelIoEx` 與第二次等待之前先保存逾時/失敗的原始錯誤碼,不再被後續呼叫覆寫; 第二次等待仍為必要:`OVERLAPPED` 位於呼叫端堆疊,I/O 未完成前不得返回,`CancelIoEx` 唯一的失敗情境(`ERROR_NOT_FOUND`)代表 I/O 已完成、事件已觸發。 - `_ConnectServerPipe()` 檢查 `WaitForSingleObject` 結果,失敗即取消 I/O、關閉管線並拋出。 - `_Receive()` 失敗(含逾時)時關閉客戶端的管線連線:被放棄的回應否則會留在管線中,成為下一筆請求讀到的回應;下一次交易自動重連,伺服器端工作執行緒於讀寫失敗時自行收尾。
`Refresh()` 在托盤執行緒修改 `CSystemTray` 狀態(`m_tnd`、`m_bHidden` 等),而訊息執行緒仍會在工作列重建(`InstallIconPending()`)與托盤通知(`OnTrayNotification()`)時觸及同一組狀態。 三個進入點一律持 `m_tray_mutex`;採 recursive mutex,因托盤選單的模態迴圈可能在持鎖期間派送工作列重建訊息。 `CSystemTray::InstallIconPending()` 改為 virtual 以便覆寫。
Author
|
已依 Copilot 的審查補了兩個提交(8d429d9、1a853c7):
|
Wujidadi
added a commit
to Wujidadi/weasel
that referenced
this pull request
Aug 22, 2026
- `OverlappedOp` 於 `CreateEvent` 失敗時立即拋出,避免後續以空事件等待。 - `_WaitIo()` 在 `CancelIoEx` 與第二次等待之前先保存逾時/失敗的原始錯誤碼,不再被後續呼叫覆寫; 第二次等待仍為必要:`OVERLAPPED` 位於呼叫端堆疊,I/O 未完成前不得返回,`CancelIoEx` 唯一的失敗情境(`ERROR_NOT_FOUND`)代表 I/O 已完成、事件已觸發。 - `_ConnectServerPipe()` 檢查 `WaitForSingleObject` 結果,失敗即取消 I/O、關閉管線並拋出。 - `_Receive()` 失敗(含逾時)時關閉客戶端的管線連線:被放棄的回應否則會留在管線中,成為下一筆請求讀到的回應;下一次交易自動重連,伺服器端工作執行緒於讀寫失敗時自行收尾。 (cherry picked from commit 8d429d9)
Wujidadi
added a commit
to Wujidadi/weasel
that referenced
this pull request
Aug 22, 2026
- build.bat 的 fork 版號後綴進位 -wujidadi.4 - FORK-CHANGELOG 新增 0.17.4-wujidadi.4 節,記錄自 rime#1914 分支揀回的四筆修正 - appcast 與 testing-appcast 條目更新至 0.17.4-wujidadi.4
fcxxxz
added a commit
to fcxxxz/weasel
that referenced
this pull request
Sep 1, 2026
按用户指令恢复 GPU 优先/WARP 兜底(WEASEL_WARP=1 可选软渲染): CPU 满载时渲染不与系统抢 CPU,全屏布局+阴影在 4K 下必须 GPU (对抗审查 rime#7 实锤该场景软渲染为每键整屏 CPU 光栅化)。 服务器端已接受管道改阻塞读写:worker 线程可弃,无需每请求付 overlapped 事件跳转;客户端完整保留 overlapped+分级超时,rime#1914 防冻语义不变。审查确认无死锁/帧错位回归。 对抗性审查(18 项)落地四项修复: - Present 改回 (0,0):每帧等 vblank 最高 8ms 打字卡顿,TSF 进程内 模式下卡的是宿主应用 UI 线程(rime#1) - _ReceiveSync 删短读分支:防旧版客户端消息被当正文吞掉造成帧 错位(rime#10) - Initialize 先销毁旧保活会话:防部署后双初始化泄漏(rime#14) - Prewarm 应用待定尺寸 + 隐藏期钳位用待定尺寸(rime#2/rime#3) GPU 模式验证:p50 40µs、会话 3ms、切换 1ms、句柄 30k 键稳定; 单测/parser 门全绿;墨菲压测(90k 键+并发+强杀客户端)全过。
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.
概要
補 #1909 的客戶端那一半,與已合併的 #1912 互補。
#1912 把托盤刷新移到伺服器訊息執行緒,斷開了「explorer 工作列執行緒等管線回應 ↔ WeaselServer 管線工作執行緒等
Shell_NotifyIcon」的死結環,解決了最常見的 7~8 秒凍結。但
WeaselIPC客戶端本身仍是無限等待:TSF 模組載入於每個宿主行程,焦點切換與按鍵的 IPC 往返都跑在宿主 UI 執行緒上,只要伺服器因任何原因停頓,宿主視窗就被一起拖死。本 PR 讓跑在宿主執行緒上的客戶端 IPC 一律有上限,伺服器端呼叫路徑維持原行為。
現況問題(
WeaselIPC/PipeChannel.cpp)_Connect()以while (_Invalid(pipe = _TryConnect())) ::WaitNamedPipe(name, 500);無限重試——伺服器忙到掛不出管線執行個體時,宿主 UI 執行緒永遠等下去。_Receive()/_WritePipe()用同步ReadFile/WriteFile,無逾時;連上之後伺服器不回應即死等(WeaselIPC客戶端在宿主程式 UI 執行緒上無限等待伺服器,切換視窗時造成卡頓或視窗凍結 7~8 秒 #1909 與 weasel.dll 导致应用进程卡死,附详细堆栈信息 #1878 的傾印都停在這裡的WaitForSingleObject(hPipe, INFINITE))。_WritePipe()內的FlushFileBuffers在具名管線上會等到對端讀走才返回,伺服器忙碌時成為額外阻塞點;request/response 模式已有同步點,此呼叫無必要。#1912 之後仍會觸發上述等待的情境:
SyncUserData、大型 userdb 寫入等使g_api_mutex長時間被持有。RimeWithWeaselHandler::FocusIn→_UpdateUI()仍在管線工作執行緒上呼叫m_ui->Hide()/m_ui->Update();後者在 ctx/status 改變時會走WeaselPanel::Refresh()的SetWindowPos,而候選視窗屬伺服器訊息執行緒,跨執行緒SetWindowPos/ShowWindow是同步送訊息。若訊息執行緒此刻正在Shell_NotifyIcon(內部SendMessageTimeout帶SMTO_BLOCK,期間不處理送入訊息),原本的環仍可短暫成立。修改內容
三個提交,可獨立審閱:
1.
WeaselIPC客戶端有界等待全部收斂在
WeaselIPC,不動 TSF 端語義:_Connect()改為 fail-fast:一次連不上即丟ERROR_PIPE_BUSY,由既有_Ensure()接住回傳失敗;移除WaitNamedPipe迴圈。FILE_FLAG_OVERLAPPED開啟;新增_WaitIo()對讀寫套WaitForSingleObject逾時,逾時即CancelIoEx視同斷線。ERROR_MORE_DATA兩段式讀取語義保留。_WritePipe()的FlushFileBuffers。_ConnectServerPipe()配合改用 OVERLAPPED 的ConnectNamedPipe(等待仍為 INFINITE),並修掉失敗路徑的控制代碼洩漏。_WritePipe()/_Receive()/Transact()新增timeout_ms參數,預設INFINITE,伺服器端呼叫不受影響。WeaselClientImpl.cpp的_TimeoutForCommand()):PROCESS_KEY_EVENT、COMMIT、選字、翻頁等):kClientIoTimeoutMs,確保按鍵不因短暫忙碌被放行。ECHO、FOCUS_IN/FOCUS_OUT、START_SESSION/END_SESSION、START_MAINTENANCE/END_MAINTENANCE、UPDATE_INPUT_POS、TRAY_COMMAND,以及OnSetThreadFocus送出的 keycode 0 焦點刷新探測):kClientFocusTimeoutMs,漏掉一次下次事件自動補上。失敗路徑:所有逾時/錯誤以
DWORD例外丟出,由ClientImpl::_SendMessage既有的catch (DWORD)接住回 0,輸入法安靜降級(該次按鍵放行、下次自動重連),宿主程式不受影響。2.
_Send()重連後改用新控制代碼PipeChannel::_Send()的catch區塊在_Reconnect()後仍以按值傳入的舊控制代碼重送,該次重送注定失敗;改為取_GetPipeHandle()重送。3. 托盤刷新改於專用執行緒(#1912 的後續)
#1912 把
Shell_NotifyIcon移到伺服器訊息執行緒,但訊息執行緒同時擁有候選視窗:管線工作執行緒在FocusIn→_UpdateUI()仍會對候選視窗做跨執行緒ShowWindow/SetWindowPos,同步送訊息給訊息執行緒;而Shell_NotifyIcon內部以SMTO_BLOCK等工作列、期間不處理送入訊息。當工作列 UI 執行緒正在等管線回應時,環仍可能成立(只在焦點切換伴隨 UI 狀態改變時,窗口比原本窄)。WeaselTrayIcon改由專用執行緒套用刷新:RequestRefresh()存快照並喚醒,RefreshThreadProc()合併待處理請求後呼叫Refresh(),DisableRefresh()/解構子停止並 join。Shell_NotifyIcon可自任一執行緒呼叫,回呼訊息仍送至hWnd所屬的訊息執行緒。WM_WEASEL_SERVICE_NOTIFY、ServerImpl::OnServiceNotifyMessage、Server::SetTrayRefreshCallback。_RefreshTrayIcon()對explorer.exe延遲 100 ms 另開執行緒的「Dangerous, don't touch」繞道:回呼已不阻塞,對任何客戶端同步呼叫皆安全。若維護者希望保留 #1912 的訊息執行緒結構,第 3 個提交可單獨拆出另議。
逾時值
目前提交的值為
kClientIoTimeoutMs = 500、kClientFocusTimeoutMs = 25,是在 #1909 的環境(Windows 11 25H2、0.17.4)實測肉眼無感且輸入正常的值。這兩個常數開放討論:若偏好保守,可改為焦點 100 ms/按鍵 1000~2000 ms,或改成可由
weasel.yaml設定;我可依回饋調整。測試
d73f629後重建)亦正常。weasel.dll)——程式碼路徑相同,但未在實機驗證。(我沒有這種實機😭)相關
WeaselIPC客戶端在宿主程式 UI 執行緒上無限等待伺服器,切換視窗時造成卡頓或視窗凍結 7~8 秒 #1909(本 PR 對應的 issue,已隨 fix(WeaselServer): avoid tray refresh blocking IPC pipe #1912 關閉)_WritePipe在主執行緒阻塞,本 PR 的有界等待可涵蓋)TryConnect()/TryTransact();本 PR 只取最小必要改動)🤖 Generated with Claude Code