Repository navigation
Conversation
added 2 commits
September 29, 2026 12:03
…covery from blocking the thread it rescues The black-frame/reproducible-freeze reports survived several rounds of fixing because the instrument added to catch them could not report. Windows itself does classify this app as hung (Application Hang, event 1002, twice on 2026-08-23/24), so the mechanism is real — but the v2.0.7 probe asked Tauri for window state before logging, and Tauri window getters dispatch to the main thread and wait. During a stall that thread is exactly the one not answering, so the probe hung inside its own capture step and no line was ever written. "No [hangwatch] entries" was therefore never evidence of "no stall" — the unmeasured case was the very case under investigation. Watchdog (lib.rs): - Resolve the HWND via Win32 instead of Tauri: EnumWindows filtered to this process, visible, unowned. Cache it after the first success. No dispatcher round trip on any path. - Log BEFORE any other work, and read visibility with Win32 IsWindowVisible (a plain read) rather than is_visible(). - Report the stall duration, repeating every 10 s while it persists. The first report back-dates by one sampling period instead of claiming 0 ms, so "wedged 3 s" is distinguishable from "wedged until the user killed it". - SEND_TIMEOUT_MS (1.5 s) now sits below SAMPLE_MS (2 s); the old 2 s timeout against a 2 s period drifted a whole tick behind. Frame recompose (nudge_window_frame): - The cooldown stamped its timestamp before testing it, so sub-second churn (pointer crossing the window, focus flapping) kept refreshing the stamp and locked the recompose out precisely when a burst of events made it necessary. Read, decide, then claim. - Drop SetWindowPos(SWP_FRAMECHANGED): per MSDN it sends WM_NCCALCSIZE synchronously even when the size is unchanged, and SetWindowPos always sends WM_WINDOWPOSCHANGED — the rescue path was posting synchronous messages to the thread it was meant to rescue. RedrawWindow with RDW_FRAME | RDW_ALLCHILDREN repaints frame and children with no round trip. Recompose timing / UI-thread hygiene: - show() dispatches and returns immediately, so the repaint ran before the window was mapped: a no-op that still consumed the cooldown slot, leaving the recovery locked out by the time the window appeared. It now waits on a background thread until the window is really visible. - window_close() is async + spawn_blocking. Tauri maintainers single out close() as the one window operation that must not run on the UI thread. - on_window_event's focus branch no longer calls a dispatching lookup: inside that callback the main thread is the caller, so waiting on a dispatch back to it is a self-deadlock. Verified: no Rust toolchain on this machine, so compilation is validated by CI (build-shell.yml, windows-latest). Local review covered brace balance, every call site of the replaced helpers, and removal of the superseded main_hwnd().
…7 *const c_void is not Send)
CI caught what local review could not (no Rust toolchain on the reporting
machine): a raw pointer is not Send, so moving it into std::thread::spawn
does not compile.
error[E0277]: *const c_void cannot be sent between threads safely
--> src\lib.rs:404:24
The handle now crosses the boundary as usize and is cast back inside the
closure. The window outlives the thread, so the address stays valid. Same
pattern already used for MAIN_HWND_CACHE and the watchdog's cached handle.
This was the only compile error in the run.
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.
背景:为什么"修了几次还在犯"
黑屏/卡死反复出现的结构性原因是:为抓它而加的探针写不出日志。
Windows 自己已判定过这个应用"无响应"(Application Hang,事件 ID 1002,2026-08-23 与 08-24 各一次),所以机制真实存在。但 v2.0.7 的
[hangwatch]在写日志之前先问 Tauri 要窗口可见性,而 Tauri 的窗口 getter 会派发回主线程并等待应答——卡死时那个线程正是不应答的那个。于是探针卡在自己的取证步骤里,一条都没写出来。所以"日志里没有
[hangwatch]"从来不能证明"没发生卡死":没被测量到的情形,恰恰就是要调查的那一种。改动
1. 让探针在卡死时真的能报告
EnumWindows(按本进程 + 可见 + 无属主过滤)获取并缓存,全程零派发;不再走get_webview_windowIsWindowVisible(纯读取)SEND_TIMEOUT_MS(1.5s) 降到SAMPLE_MS(2s) 之下;旧的 2s 超时配 2s 周期会整整晚一个节拍2. 救援手段不再阻塞它要救的线程
nudge_window_frame的冷却先写时间戳再判断,导致亚秒级抖动(鼠标划过窗口、焦点抖动)不断刷新时间戳,恰好在事件密集、最需要它时被锁死。改为先读、判断、再落章SetWindowPos(SWP_FRAMECHANGED):按 MSDN 它即使尺寸未变也会同步发送WM_NCCALCSIZE,且SetWindowPos总会发送WM_WINDOWPOSCHANGED——救援路径在往它要救的线程发同步消息。改用RedrawWindow+RDW_FRAME | RDW_ALLCHILDREN,重绘边框与子窗口且无往返3. 时机与 UI 线程卫生
show()是派发后立即返回,所以重绘跑在窗口尚未映射时:空操作,却仍消耗了冷却名额,等窗口真正出现时恢复已被锁死。改为后台线程等到窗口真正可见再执行window_close()→async + spawn_blocking。Tauri 维护者明确指出close()是唯一必须离开 UI 线程的窗口操作on_window_event的 focus 分支不再调用会派发的查找:该回调本身运行在主线程,回调内等待"派发回主线程"就是等自己(自死锁)验证
本机无 Rust 工具链(
cargo/rustup/MSVC 均不存在),因此以 CI(build-shell.yml,windows-latest)作为编译验证。本地人工审查覆盖:括号平衡、被替换辅助函数的每一处调用点、已废弃main_hwnd()的移除。说明
这些改动让下一条证据能被写出来,还不等于"已证明治好"。真正的验证是:下次黑屏时日志中出现带持续时长的
[hangwatch]记录。若届时仍无记录,则是强证据指向 DWM/合成层,排查方向需要整体更换。同时并存一个独立故障(不在本 PR 范围):后端 3080 Node 进程周期性 CPU 饱和(
dsh-client-modules的compose/newlineCount逐字符迭代 +partitionComboRecords的 O(n²) URL 构造)。