修复线程bug,进度条结束后崩溃问题
parent
217290a76e
commit
8d489a499f
|
|
@ -138,38 +138,23 @@ class SQLServerWriter:
|
|||
start_time = data.get('start_time')
|
||||
end_time = data.get('end_time')
|
||||
|
||||
# 检查记录是否已存在(基于 order_no, start_time, end_time 组合)
|
||||
# 检查记录是否已存在
|
||||
# 目前数据库在 pump_600_no_load_run_in 上的唯一键约束是基于工单号(order_no),
|
||||
# 所以这里优先按 order_no 检查是否存在,避免与数据库唯一键不一致导致插入冲突。
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
# 构建查询条件
|
||||
if start_time and end_time:
|
||||
# 如果有开始和结束时间,使用三个字段组合查询
|
||||
cursor.execute(
|
||||
"""SELECT id FROM pump_600_no_load_run_in
|
||||
WHERE order_no = ? AND start_time = ? AND end_time = ?""",
|
||||
(order_no, start_time, end_time)
|
||||
)
|
||||
elif start_time:
|
||||
# 只有开始时间
|
||||
cursor.execute(
|
||||
"""SELECT id FROM pump_600_no_load_run_in
|
||||
WHERE order_no = ? AND start_time = ?""",
|
||||
(order_no, start_time)
|
||||
)
|
||||
else:
|
||||
# 只有工单号(向后兼容)
|
||||
cursor.execute(
|
||||
"SELECT id FROM pump_600_no_load_run_in WHERE order_no = ?",
|
||||
(order_no,)
|
||||
)
|
||||
cursor.execute(
|
||||
"SELECT id, start_time, end_time FROM pump_600_no_load_run_in WHERE order_no = ?",
|
||||
(order_no,)
|
||||
)
|
||||
|
||||
existing = cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
existing_id = existing[0]
|
||||
existing_id, existing_start, existing_end = existing
|
||||
logger.info(
|
||||
f"找到已存在的记录 (id={existing_id}, order_no={order_no}, "
|
||||
f"start_time={start_time}, end_time={end_time}),将更新数据"
|
||||
f"start_time={existing_start}, end_time={existing_end}),将更新数据 "
|
||||
f"(新 start_time={start_time}, 新 end_time={end_time})"
|
||||
)
|
||||
return self._update_pump_600_data_by_id(existing_id, data)
|
||||
else:
|
||||
|
|
@ -277,10 +262,54 @@ class SQLServerWriter:
|
|||
return True
|
||||
|
||||
except Exception as e:
|
||||
# 处理唯一键冲突:如果工单号已存在,则回退为更新逻辑
|
||||
import pyodbc as _pyodbc # 局部导入以避免类型检查器告警
|
||||
is_integrity_error = isinstance(e, _pyodbc.IntegrityError)
|
||||
error_text = str(e)
|
||||
if is_integrity_error and ("2627" in error_text or "2601" in error_text):
|
||||
order_no = data.get('order_no')
|
||||
logger.warning(
|
||||
f"插入 SQL Server 数据时发生唯一键冲突,将尝试改为更新记录 "
|
||||
f"(order_no={order_no}, 错误={error_text})"
|
||||
)
|
||||
try:
|
||||
# 查找已存在记录的ID(按唯一键 order_no)
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute(
|
||||
"SELECT id FROM pump_600_no_load_run_in WHERE order_no = ?",
|
||||
(order_no,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
record_id = row[0]
|
||||
logger.info(
|
||||
f"找到冲突记录 (id={record_id}, order_no={order_no}),执行更新替代插入"
|
||||
)
|
||||
# 回滚当前事务后执行更新
|
||||
try:
|
||||
self.connection.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return self._update_pump_600_data_by_id(record_id, data)
|
||||
else:
|
||||
logger.error(
|
||||
f"唯一键冲突但未能找到已存在记录 (order_no={order_no}),放弃更新"
|
||||
)
|
||||
except Exception as e2:
|
||||
logger.error(
|
||||
f"处理唯一键冲突为更新时失败: {e2}",
|
||||
exc_info=True
|
||||
)
|
||||
try:
|
||||
self.connection.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
logger.error(f"插入 SQL Server 数据失败: {e}", exc_info=True)
|
||||
try:
|
||||
self.connection.rollback()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
|
|
|||
104
ui_main.py
104
ui_main.py
|
|
@ -1118,6 +1118,7 @@ class MainWindow(QMainWindow):
|
|||
# 定义信号用于线程安全的UI更新
|
||||
state_changed_signal = Signal(str, str) # old_state, new_state
|
||||
connection_changed_signal = Signal(bool, str) # is_connected, message
|
||||
reload_experiments_signal = Signal()
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -1133,6 +1134,8 @@ class MainWindow(QMainWindow):
|
|||
# 连接信号到槽函数
|
||||
self.state_changed_signal.connect(self._handle_state_change_in_main_thread)
|
||||
self.connection_changed_signal.connect(self._handle_connection_change_in_main_thread)
|
||||
# 刷新实验列表必须在主线程执行(后台线程通过 emit 请求刷新)
|
||||
self.reload_experiments_signal.connect(self._reload_experiments, Qt.QueuedConnection)
|
||||
|
||||
self.template_path: Path | None = None
|
||||
self.config: AppConfig = AppConfig()
|
||||
|
|
@ -2639,8 +2642,20 @@ class MainWindow(QMainWindow):
|
|||
thread.quit()
|
||||
# 使用QTimer延迟等待,避免在信号槽中直接调用wait导致死锁
|
||||
def do_wait():
|
||||
# 线程还在跑就继续等,避免 “QThread: Destroyed while thread is still running”
|
||||
if thread.isRunning():
|
||||
thread.wait(100) # 最多等待100ms
|
||||
# 不在这里强行 deleteLater,等线程真正结束后再清理
|
||||
QTimer.singleShot(50, do_wait)
|
||||
return
|
||||
# 线程已结束:请求在其所属线程中安全销毁
|
||||
try:
|
||||
worker.deleteLater()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
thread.deleteLater()
|
||||
except Exception:
|
||||
pass
|
||||
# 从列表中移除
|
||||
try:
|
||||
self._bg_threads.remove(thread)
|
||||
|
|
@ -2663,7 +2678,10 @@ class MainWindow(QMainWindow):
|
|||
progress.show()
|
||||
|
||||
bridge = ProgressBridge()
|
||||
bridge.updated.connect(lambda msg, cur, total: progress.setLabelText(f"{msg} ({cur}/{total})"))
|
||||
# 进度更新也可能来自后台线程,必须使用 QueuedConnection
|
||||
def _on_progress_updated(msg: str, cur: int, total: int) -> None:
|
||||
progress.setLabelText(f"{msg} ({cur}/{total})")
|
||||
bridge.updated.connect(_on_progress_updated, Qt.QueuedConnection)
|
||||
|
||||
def _progress_text(msg: str, cur: int, total: int) -> None:
|
||||
bridge.updated.emit(msg, cur, total)
|
||||
|
|
@ -2683,6 +2701,11 @@ class MainWindow(QMainWindow):
|
|||
set_progress_callback(None)
|
||||
if not progress.wasCanceled() and not cancelled[0]:
|
||||
progress.close()
|
||||
# 确保在主线程事件循环中销毁对话框,避免跨线程析构告警
|
||||
try:
|
||||
progress.deleteLater()
|
||||
except Exception:
|
||||
pass
|
||||
# 使用安全的清理方法
|
||||
self._cleanup_thread_safe(thread, worker)
|
||||
if cancelled[0]:
|
||||
|
|
@ -2703,9 +2726,29 @@ class MainWindow(QMainWindow):
|
|||
|
||||
progress.canceled.connect(on_cancel)
|
||||
|
||||
worker.finished.connect(lambda res: (None if cancelled[0] else (_cleanup(True), on_success(res))))
|
||||
worker.failed.connect(lambda msg: (None if cancelled[0] else (_cleanup(), (self.logger.error("Task failed: %s", msg), QMessageBox.critical(self, "错误", msg)))))
|
||||
worker.cancelled.connect(lambda: (_cleanup() if not cancelled[0] else None))
|
||||
# 注意:worker 的信号在其所属线程中发射。为了避免跨线程操作 Qt UI(会导致卡死/警告),
|
||||
# 这里必须使用 QueuedConnection,确保回调在主线程事件循环中执行。
|
||||
def on_finished(res):
|
||||
if cancelled[0]:
|
||||
return
|
||||
_cleanup(True)
|
||||
on_success(res)
|
||||
|
||||
def on_failed(msg):
|
||||
if cancelled[0]:
|
||||
return
|
||||
_cleanup()
|
||||
self.logger.error("Task failed: %s", msg)
|
||||
QMessageBox.critical(self, "错误", msg)
|
||||
|
||||
def on_cancelled():
|
||||
if cancelled[0]:
|
||||
return
|
||||
_cleanup()
|
||||
|
||||
worker.finished.connect(on_finished, Qt.QueuedConnection)
|
||||
worker.failed.connect(on_failed, Qt.QueuedConnection)
|
||||
worker.cancelled.connect(on_cancelled, Qt.QueuedConnection)
|
||||
self._bg_threads.append(thread)
|
||||
self._bg_workers.append(worker)
|
||||
thread.start()
|
||||
|
|
@ -3906,6 +3949,18 @@ class MainWindow(QMainWindow):
|
|||
self._reload_experiments()
|
||||
|
||||
def _reload_experiments(self) -> None:
|
||||
# 保护:如果被后台线程误调用,立即切回主线程执行
|
||||
try:
|
||||
if QThread.currentThread() != self.thread():
|
||||
try:
|
||||
self.reload_experiments_signal.emit()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
except Exception:
|
||||
# 线程检查失败时继续尝试执行(但正常情况下不会发生)
|
||||
pass
|
||||
|
||||
# 更新配置对话框中的实验表格(如果存在)
|
||||
if hasattr(self, "exp_table") and self.exp_table is not None:
|
||||
try:
|
||||
|
|
@ -4687,9 +4742,11 @@ class MainWindow(QMainWindow):
|
|||
db.close()
|
||||
|
||||
self.logger.info(f"[SQL Server] 更新实验 {exp_id} 状态为: {status}")
|
||||
|
||||
# 刷新列表显示
|
||||
self._reload_experiments()
|
||||
# 刷新列表显示:可能从后台线程调用,必须通过信号切回主线程
|
||||
try:
|
||||
self.reload_experiments_signal.emit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[SQL Server] 更新状态失败: {e}", exc_info=True)
|
||||
|
|
@ -6071,6 +6128,37 @@ def run_app() -> None:
|
|||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# 安装 Qt 消息处理器:当出现 “different thread” 等警告时打印 Python 调用栈与线程信息,
|
||||
# 用于精确定位是哪段代码在后台线程操作了 UI。
|
||||
try:
|
||||
import threading
|
||||
import traceback
|
||||
from PySide6.QtCore import qInstallMessageHandler
|
||||
from logger import get_logger
|
||||
|
||||
_qt_logger = get_logger("qt")
|
||||
|
||||
def _qt_message_handler(msg_type, context, message):
|
||||
try:
|
||||
text = str(message)
|
||||
# 只对高风险跨线程告警输出堆栈,避免日志爆炸
|
||||
if "different thread" in text or "Timers cannot be started from another thread" in text:
|
||||
_qt_logger.error(
|
||||
"QtThreadWarning: %s | thread=%s (%s)",
|
||||
text,
|
||||
threading.current_thread().name,
|
||||
threading.get_ident(),
|
||||
)
|
||||
_qt_logger.error("QtThreadWarning stack:\n%s", "".join(traceback.format_stack(limit=40)))
|
||||
else:
|
||||
_qt_logger.warning("Qt: %s", text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
qInstallMessageHandler(_qt_message_handler)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 显示启动界面
|
||||
from splash_screen import SplashScreen
|
||||
|
|
|
|||
Loading…
Reference in New Issue