# -*- coding: utf-8 -*- """ Action 执行器 ───────────────────────────────────────────────────────────────── 基于 PyQt5 QThreadPool + pyqtSignal 实现: - 异步提交 Action(不阻塞主线程) - 处理函数产生的 Event 自动提交到 EventBus - 超时检测(QTimer 监控) - 优先级队列 - 执行历史记录 ───────────────────────────────────────────────────────────────── """ from __future__ import annotations import logging import threading import time from collections import deque from typing import Dict, List from PyQt5.QtCore import ( QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot, QTimer ) from uas.action.action_context import ActionContext, ActionCancelledError from uas.action.action_model import Action, ActionResult, ActionState from uas.action.action_type import action_registry logger = logging.getLogger(__name__) # ───────────────────────────────────────────────────────────────── # Worker 信号载体 # ───────────────────────────────────────────────────────────────── class _WorkerSignals(QObject): finished = pyqtSignal(object) # Action failed = pyqtSignal(object, str) # Action, error_msg progress = pyqtSignal(str, int, str) # action_id, percent, message events_ready = pyqtSignal(object, list) # Action, [Event, ...] # ───────────────────────────────────────────────────────────────── # Action Worker(在 QThreadPool 工作线程中执行) # ───────────────────────────────────────────────────────────────── class _ActionWorker(QRunnable): """ 在 QThreadPool 工作线程中执行单个 Action 执行完成后通过信号将产生的 Event 列表回传主线程 """ def __init__(self, action: Action, shared_store: dict): super().__init__() self.action = action self.shared_store = shared_store self.signals = _WorkerSignals() self.setAutoDelete(True) @pyqtSlot() def run(self): action = self.action action.mark_running() # 构建执行上下文 ctx = ActionContext( action = action, shared_store = self.shared_store, progress_cb = lambda aid, pct, msg: self.signals.progress.emit(aid, pct, msg), ) t0 = time.perf_counter() try: if action.handler is None: raise ValueError( f"Action [{action.type_key}] 未设置处理函数 handler" ) # ── 执行处理函数 ────────────────────────────────────── result_data = action.handler(action, ctx) elapsed = (time.perf_counter() - t0) * 1000 # 收集处理函数产生的 Event events = ctx.collected_events result = ActionResult( action_id = action.action_id, action_type = action.type_key, success = True, data = result_data, elapsed_ms = elapsed, events = events, ) action.mark_completed(result) # 先发射 events_ready,再发射 finished if events: self.signals.events_ready.emit(action, events) self.signals.finished.emit(action) logger.debug( f"Action 完成: {action} elapsed={elapsed:.1f}ms " f"events={len(events)}" ) except ActionCancelledError as e: action.mark_cancelled() self.signals.finished.emit(action) logger.info(f"Action 已取消: {action}") except Exception as e: elapsed = (time.perf_counter() - t0) * 1000 action.mark_failed(str(e)) self.signals.failed.emit(action, str(e)) logger.error(f"Action 执行失败: {action} error={e}", exc_info=True) # ───────────────────────────────────────────────────────────────── # Action 执行器主类 # ───────────────────────────────────────────────────────────────── class ActionExecutor(QObject): """ Action 执行器(单例) ───────────────────────────────────────────────────────────── 使用方式: executor = ActionExecutor.instance() # 异步提交 executor.submit(my_action) # 同步提交(阻塞等待) result = executor.submit_sync(my_action) # 取消 executor.cancel(action_id) ───────────────────────────────────────────────────────────── """ _instance = None _lock = threading.Lock() # ── 对外信号 ────────────────────────────────────────────────── action_submitted = pyqtSignal(object) # Action action_started = pyqtSignal(object) # Action action_completed = pyqtSignal(object) # Action action_failed = pyqtSignal(object, str) # Action, error action_cancelled = pyqtSignal(object) # Action action_progress = pyqtSignal(str, int, str) # id, percent, msg events_produced = pyqtSignal(object, list) # Action, [Event] stats_updated = pyqtSignal(dict) # 统计字典 @classmethod def instance(cls) -> "ActionExecutor": with cls._lock: if cls._instance is None: cls._instance = cls() return cls._instance def __init__(self, max_threads: int = 8, history_size: int = 200, parent=None): super().__init__(parent) self._pool: QThreadPool = QThreadPool.globalInstance() self._pool.setMaxThreadCount(max_threads) # 运行中的 Action { action_id: Action } self._running: Dict[str, Action] = {} self._running_lock = threading.Lock() # 超时监控定时器 { action_id: QTimer } self._timers: Dict[str, QTimer] = {} # 历史记录 self._history: deque[Action] = deque(maxlen=history_size) # 共享数据存储(跨 Action 共享) self._shared_store: dict = {} # 统计 self._stats = { "submitted": 0, "completed": 0, "failed": 0, "cancelled": 0 } # EventBus 引用(延迟导入避免循环依赖) self._event_bus = None logger.info(f"✅ ActionExecutor 初始化 max_threads={max_threads}") def set_event_bus(self, bus) -> None: """注入 EventBus(用于自动提交 Action 产生的 Event)""" self._event_bus = bus # 连接 events_produced 信号到自动提交槽 self.events_produced.connect(self._auto_post_events) logger.info("✅ EventBus 已注入 ActionExecutor") # ── 提交接口 ────────────────────────────────────────────────── def submit(self, action: Action) -> str: """ 异步提交 Action(立即返回,不阻塞主线程) :return: action_id """ action.mark_queued() self._stats["submitted"] += 1 self._add_history(action) self.action_submitted.emit(action) # 注册超时监控 self._start_timeout_timer(action) # 提交到线程池 worker = _ActionWorker(action, self._shared_store) worker.signals.finished.connect(self._on_worker_finished) worker.signals.failed.connect(self._on_worker_failed) worker.signals.progress.connect(self._on_worker_progress) worker.signals.events_ready.connect(self._on_events_ready) with self._running_lock: self._running[action.action_id] = action self._pool.start(worker) logger.debug(f"异步提交 Action: {action}") return action.action_id def submit_sync(self, action: Action, timeout_sec: float = 30.0) -> ActionResult: """ 同步提交 Action(阻塞等待完成) :return: ActionResult """ action.mark_queued() action.mark_running() self._stats["submitted"] += 1 self._add_history(action) ctx = ActionContext(action=action, shared_store=self._shared_store) t0 = time.perf_counter() try: if action.handler is None: raise ValueError(f"Action [{action.type_key}] 未设置 handler") result_data = action.handler(action, ctx) elapsed = (time.perf_counter() - t0) * 1000 events = ctx.collected_events result = ActionResult( action_id = action.action_id, action_type = action.type_key, success = True, data = result_data, elapsed_ms = elapsed, events = events, ) action.mark_completed(result) self._stats["completed"] += 1 # 同步模式下直接提交 Event if self._event_bus and events: for event in events: self._event_bus.post(event) self.action_completed.emit(action) self._emit_stats() return result except ActionCancelledError: action.mark_cancelled() self._stats["cancelled"] += 1 self.action_cancelled.emit(action) return ActionResult( action_id=action.action_id, action_type=action.type_key, success=False, error="已取消" ) except Exception as e: action.mark_failed(str(e)) self._stats["failed"] += 1 self.action_failed.emit(action, str(e)) return action.result def cancel(self, action_id: str) -> bool: """取消指定 Action""" with self._running_lock: action = self._running.get(action_id) if action: return action.request_cancel() return False def cancel_all(self) -> int: """取消所有运行中的 Action""" with self._running_lock: actions = list(self._running.values()) count = sum(1 for a in actions if a.request_cancel()) logger.info(f"取消 {count} 个 Action") return count # ── 超时监控 ────────────────────────────────────────────────── def _start_timeout_timer(self, action: Action): meta = action_registry.get(action.type_key) if not meta: return timeout_ms = int(meta.timeout_sec * 1000) timer = QTimer(self) timer.setSingleShot(True) timer.timeout.connect(lambda: self._on_timeout(action.action_id)) timer.start(timeout_ms) self._timers[action.action_id] = timer def _stop_timeout_timer(self, action_id: str): timer = self._timers.pop(action_id, None) if timer: timer.stop() timer.deleteLater() @pyqtSlot() def _on_timeout(self, action_id: str = None): with self._running_lock: action = self._running.get(action_id) if action and action.is_running: action.mark_timeout() action.request_cancel() logger.warning(f"⏱️ Action 超时: {action}") self.action_failed.emit(action, "执行超时") # ── Worker 槽函数 ───────────────────────────────────────────── @pyqtSlot(object) def _on_worker_finished(self, action: object): self._stop_timeout_timer(action.action_id) with self._running_lock: self._running.pop(action.action_id, None) if action.state == ActionState.CANCELLED: self._stats["cancelled"] += 1 self.action_cancelled.emit(action) else: self._stats["completed"] += 1 self.action_completed.emit(action) self._emit_stats() logger.debug(f"Action 完成回调: {action}") @pyqtSlot(object, str) def _on_worker_failed(self, action: object, error: str): self._stop_timeout_timer(action.action_id) with self._running_lock: self._running.pop(action.action_id, None) self._stats["failed"] += 1 self.action_failed.emit(action, error) self._emit_stats() @pyqtSlot(str, int, str) def _on_worker_progress(self, action_id: str, percent: int, message: str): self.action_progress.emit(action_id, percent, message) @pyqtSlot(object, list) def _on_events_ready(self, action: object, events: list): self.events_produced.emit(action, events) @pyqtSlot(object, list) def _auto_post_events(self, action: object, events: list): """自动将 Action 产生的 Event 提交到 EventBus""" if self._event_bus: for event in events: self._event_bus.post(event) logger.debug( f"自动提交 Event: {event} <- Action[{action.type_key}]" ) # ── 内部工具 ────────────────────────────────────────────────── def _add_history(self, action: Action): self._history.append(action) def _emit_stats(self): self.stats_updated.emit(dict(self._stats)) # ── 查询接口 ────────────────────────────────────────────────── def get_history(self, limit: int = 50) -> List[Action]: return list(self._history)[-limit:] def get_running(self) -> List[Action]: with self._running_lock: return list(self._running.values()) def get_stats(self) -> dict: return dict(self._stats) def wait_for_done(self, timeout_ms: int = 10000): self._pool.waitForDone(timeout_ms) # 全局执行器实例 action_executor = ActionExecutor.instance()