Repository navigation
fix: 修复了线程静默退出时进度组件永久停留的问题 - #84
Merged
Merged
Conversation
## 问题 `ProcessWidget.checkProcess` 原先只在 `self.stopped` 为真时补发 `canceled`。因此若子线程因未捕获异常或漏发结束信号而退出,且用户没有点过取消,组件会一直停留在“进行中”且不再发出任何结束信号,进度条/进程对话框永久卡住。 ## 修复 使用 `_saw_end` 记录本轮是否已收到 `hasFinished/canceled`,并把结束判定挂到 `QThread.finished`(新增 `onThreadExited`): - 线程结束但从未收到成功/取消信号时,记录 warning 并按取消走统一清理,组件正常隐藏、按钮复位; - `QThread.finished` 一定在 `run()` 内的结束信号之后入队,主线程 FIFO 派发,判定不会与还在排队的信号竞速,正常完成不会被误报为取消; - `checkProcess` 不再做结束判定,`not isRunning` 分支只停表并`return`,避免停表后继续执行倒退动画/强杀逻辑; - 重复调用只上报一次,用户取消与超时强杀路径行为不变。 ## 测试 新增 `test/app/test_process_widget.py`(TEST_DOMAIN=qt-ui、TEST_REGRESSION)
## 问题 - `checkProcess` 原先只在 `stopped` 为真时补发 `canceled`,线程因未捕获异常或漏发结束信号而退出时,进度弹条会一直停在界面上。 ## 修复 - 改为用 `_saw_end` 跟踪本轮结束信号,并挂 `QThread.finished`(`onThreadExited`)统一判定; - `checkProcess` 不再做结束判定。 ## 其他修复 - `connectToThread` 断开旧线程时的两处错误信号名(`progressPause` -> `progressPaused`、`processFinish` -> `hasFinished`) ## 测试 新增 `test/app/test_progress_info_bar.py`(TEST_DOMAIN=qt-ui、TEST_REGRESSION)
fyc09
requested changes
Sep 30, 2026
fyc09
left a comment
Collaborator
There was a problem hiding this comment.
静默退出后的清理方式总体合理,但重绑定线程时对旧信号的保护还不完整。请补充对已排队旧线程信号的绑定校验,并增加“旧线程先发出信号、主线程派发前重绑定”的回归测试后再合并。这是基于代码和 Qt 排队信号机制的边界风险判断,尚未在真实 Qt 环境实机复现。
针对 PR 审查意见(P2):connectToThread() 的 disconnect() 不保证撤销已排队的 queued signal,旧线程先发出的 hasFinished / canceled / started 等信号在重绑定后仍可能进入状态槽,从而关闭新任务的进度条或重置其状态。 - 新增 _bindThreadSignal():线程的 10 个信号统一经带绑定校验的代理接收 器连接,派发时校验 _closed、sip.isdeleted(self) 与 thread is self.thread_,丢弃旧线程的迟到/已入队事件 - closeEvent 与 destroyed 显式断开接收器连接并清空,避免已关闭/已销毁 对象被长生命周期线程连接持有;迟到信号由 _closed 与 sip.isdeleted 兜底 - 补充回归测试:覆盖「旧线程先发出信号、主线程派发前重绑定」场景,含 disconnect_last=False 保留连接的真实排队路径,并以哨兵断言证明事件 确实被派发;覆盖 hasFinished / canceled / started 及全部 UI 事件
qt-ui 分片里新增的 test_progress_info_bar 首次调用 sendPostedEvents(None, QEvent.DeferredDelete),会连带销毁先前用例残留、且仍在 运行的 QThread,触发 Qt qFatal -> SIGABRT(退出码 134)。 - tearDown 先停止并等待页面辖下全部 CampusFeatureThread 子线程,再显式发送 DeferredDelete 真正销毁页面(processEvents 不会 flush 延迟删除)。 - 两个用例在 patch 范围外再次调用 showEvent,会发起真实后台网络请求(校历图片 下载,timeout=30s),这类线程卡在 DNS/请求超时时 terminate() 结束不掉;改为在 替换掉加载函数的前提下断言,既保留原意,也真正覆盖“无账号不请求”与“重新加载” 两个分支。
Contributor
Author
CI 失败已修复#57 的 CI 失败(exit 134)
根因:
本 PR 内已修复
不在本 PR 范围
|
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.
变更说明
背景
ProcessWidget(页面内进度组件)和ProgressInfoBar(右下角进度弹条)用同一套模式:checkProcess定时轮询thread.isRunning(),但只在用户点过取消(stopped=True)时补发canceled。return路径漏发hasFinished/canceled而静默退出时,两个组件都不会发出任何结束信号:进度条/弹条永久停在界面上,按钮状态也不回退。ProgressInfoBar.connectToThread(disconnect_last=True)的重绑定路径此前从未被实际执行过,其中两个信号名(progressPause、processFinish)是错误的,且没有断开QThread.finished。一旦有组件重绑定线程,旧线程结束会误伤新绑定,目前程序可以正常运行只是因为没有触及这个路径。原因
checkProcess原先的结束判定是:静默退出的线程在这一分支被直接忽略。
修复
两个组件统一引入
_saw_end跟踪本轮是否收到过hasFinished/canceled,并把结束判定改挂到QThread.finished(新增onThreadExited):ProcessWidget:线程结束仍未收到结束信号 →warning+onStopped(),走统一清理(隐藏、按钮复位、发出canceled);onThreadExited先停表再判定(该组件成功路径的onFinished不停表)。ProgressInfoBar:onProcessFinish/onStopped已停表并关闭,onThreadExited只做判定、不再触碰 timer;增加sender()防御旧线程的迟到信号;connectToThread断开旧线程时补上finished,并修正progressPause -> progressPaused、processFinish -> hasFinished。onFinished/onProcessFinish/onStopped置位_saw_end,onThreadStart/connectToThread重置;checkProcess不再做结束判定,not isRunning分支只停表并return,避免停表后继续执行倒退动画/强杀逻辑。为何要使用
QThread.finishedworker 线程内的顺序是固定的:
run()里发出的hasFinished/canceled先入队,run()返回后 Qt 才发出finished。主线程按 FIFO 派发,因此onThreadExited读取_saw_end时本轮的结束信号一定已经处理过。曾尝试过
checkProcess定时器里按isRunning()轮询判定,但定时器回调与排队信号是不同的事件源,派发顺序由 Qt 实现决定。用 1ms 定时器放大窗口测试 200 轮,旧写法出现了 1 次“正常完成被误报 canceled”改动文件
app/threads/ProcessWidget.py:_saw_end判定 +onThreadExited挂钩QThread.finished;checkProcess移除竞态判定并补return。app/components/ProgressInfoBar.py:close()/deleteLater()”的生命周期;finished断连。test/app/test_process_widget.py(新增,5 用例):新增测试模块(TEST_DOMAIN=qt-ui、TEST_REGRESSION=True)。test/app/test_progress_info_bar.py(新增,5 用例):新增测试模块(TEST_DOMAIN=qt-ui、TEST_REGRESSION=True)。验证
PR Tests / CI Gate已通过。测试证据
uv run --frozen python -m test.ci.check_test_contract # OK: 29 product test modules are covered by 5 domains.