398 lines
15 KiB
Python
398 lines
15 KiB
Python
|
|
# -*- 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()
|