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.
| 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 以便覆寫。
|
已依 Copilot 的審查補了兩個提交(8d429d9、1a853c7):
|
- `OverlappedOp` 於 `CreateEvent` 失敗時立即拋出,避免後續以空事件等待。 - `_WaitIo()` 在 `CancelIoEx` 與第二次等待之前先保存逾時/失敗的原始錯誤碼,不再被後續呼叫覆寫; 第二次等待仍為必要:`OVERLAPPED` 位於呼叫端堆疊,I/O 未完成前不得返回,`CancelIoEx` 唯一的失敗情境(`ERROR_NOT_FOUND`)代表 I/O 已完成、事件已觸發。 - `_ConnectServerPipe()` 檢查 `WaitForSingleObject` 結果,失敗即取消 I/O、關閉管線並拋出。 - `_Receive()` 失敗(含逾時)時關閉客戶端的管線連線:被放棄的回應否則會留在管線中,成為下一筆請求讀到的回應;下一次交易自動重連,伺服器端工作執行緒於讀寫失敗時自行收尾。 (cherry picked from commit 8d429d9)
- build.bat 的 fork 版號後綴進位 -wujidadi.4 - FORK-CHANGELOG 新增 0.17.4-wujidadi.4 節,記錄自 rime#1914 分支揀回的四筆修正 - appcast 與 testing-appcast 條目更新至 0.17.4-wujidadi.4
按用户指令恢复 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 键+并发+强杀客户端)全过。
|
命名管道变成无名管道? |
不是,兩端仍然是具名管道。是指
|
概要
補 #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