176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""任务执行工作线程"""
|
|
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
from typing import List, Optional, Dict
|
|
|
|
from PySide6.QtCore import QThread, Signal
|
|
|
|
from ..utils.app_settings import app_settings
|
|
|
|
|
|
class TaskWorker(QThread):
|
|
"""任务执行工作线程"""
|
|
|
|
# 信号
|
|
output_received = Signal(str) # 收到输出行
|
|
task_finished = Signal(int, str) # 任务完成 (exit_code, summary)
|
|
error_occurred = Signal(str) # 发生错误
|
|
progress_updated = Signal(int, int) # 进度更新 (current, total)
|
|
|
|
def __init__(self, command: List[str], working_dir: Optional[str] = None,
|
|
extra_env: Optional[Dict[str, str]] = None, parent=None):
|
|
super().__init__(parent)
|
|
self.command = command
|
|
self.extra_env = extra_env or {}
|
|
|
|
# 工作目录优先级: 参数 > 应用设置 > 自动检测
|
|
if working_dir is not None:
|
|
self.working_dir = working_dir
|
|
elif app_settings.etl_project_path:
|
|
self.working_dir = app_settings.etl_project_path
|
|
else:
|
|
# 回退到源码目录
|
|
self.working_dir = str(Path(__file__).resolve().parents[2])
|
|
|
|
self.process: Optional[subprocess.Popen] = None
|
|
self._stop_requested = False
|
|
self._exit_code: Optional[int] = None
|
|
self._output_lines: List[str] = []
|
|
|
|
def run(self):
|
|
"""执行任务"""
|
|
try:
|
|
self._stop_requested = False
|
|
self._output_lines = []
|
|
|
|
# 设置环境变量
|
|
env = os.environ.copy()
|
|
env["PYTHONIOENCODING"] = "utf-8"
|
|
env["PYTHONUNBUFFERED"] = "1"
|
|
|
|
# 添加项目根目录到 PYTHONPATH
|
|
project_root = self.working_dir
|
|
existing_path = env.get("PYTHONPATH", "")
|
|
if existing_path:
|
|
env["PYTHONPATH"] = f"{project_root}{os.pathsep}{existing_path}"
|
|
else:
|
|
env["PYTHONPATH"] = project_root
|
|
|
|
# 添加额外的环境变量
|
|
if self.extra_env:
|
|
for key, value in self.extra_env.items():
|
|
env[key] = str(value)
|
|
self.output_received.emit(f"[环境变量] {key}={value}")
|
|
|
|
self.output_received.emit(f"[工作目录] {self.working_dir}")
|
|
self.output_received.emit(f"[执行命令] {' '.join(self.command)}")
|
|
|
|
# 启动进程
|
|
self.process = subprocess.Popen(
|
|
self.command,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
cwd=self.working_dir,
|
|
env=env,
|
|
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0,
|
|
)
|
|
|
|
# 读取输出
|
|
if self.process.stdout:
|
|
for line in iter(self.process.stdout.readline, ""):
|
|
if self._stop_requested:
|
|
break
|
|
|
|
line = line.rstrip("\n\r")
|
|
if line:
|
|
self._output_lines.append(line)
|
|
self.output_received.emit(line)
|
|
|
|
# 解析进度信息(如果有)
|
|
self._parse_progress(line)
|
|
|
|
# 等待进程结束
|
|
if self.process:
|
|
self.process.wait()
|
|
self._exit_code = self.process.returncode
|
|
|
|
# 生成摘要
|
|
summary = self._generate_summary()
|
|
self.task_finished.emit(self._exit_code or 0, summary)
|
|
|
|
except FileNotFoundError as e:
|
|
self.error_occurred.emit(f"找不到 Python 解释器: {e}")
|
|
self.task_finished.emit(-1, f"执行失败: {e}")
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"执行出错: {e}")
|
|
self.task_finished.emit(-1, f"执行失败: {e}")
|
|
finally:
|
|
self.process = None
|
|
|
|
def stop(self):
|
|
"""停止任务"""
|
|
self._stop_requested = True
|
|
if self.process:
|
|
try:
|
|
self.process.terminate()
|
|
# 给进程一些时间来终止
|
|
try:
|
|
self.process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
self.process.kill()
|
|
except Exception:
|
|
pass
|
|
|
|
def _parse_progress(self, line: str):
|
|
"""解析进度信息"""
|
|
# 尝试从日志中解析进度
|
|
# 示例: "[INFO] 处理进度: 50/100"
|
|
import re
|
|
match = re.search(r'进度[:\s]*(\d+)/(\d+)', line)
|
|
if match:
|
|
current = int(match.group(1))
|
|
total = int(match.group(2))
|
|
self.progress_updated.emit(current, total)
|
|
|
|
def _generate_summary(self) -> str:
|
|
"""生成执行摘要"""
|
|
if not self._output_lines:
|
|
return "无输出"
|
|
|
|
# 查找关键信息
|
|
summary_parts = []
|
|
|
|
for line in self._output_lines[-20:]: # 只看最后 20 行
|
|
line_lower = line.lower()
|
|
if "success" in line_lower or "完成" in line or "成功" in line:
|
|
summary_parts.append(line)
|
|
elif "error" in line_lower or "失败" in line or "错误" in line:
|
|
summary_parts.append(line)
|
|
elif "inserted" in line_lower or "updated" in line_lower:
|
|
summary_parts.append(line)
|
|
elif "fetched" in line_lower or "抓取" in line:
|
|
summary_parts.append(line)
|
|
|
|
if summary_parts:
|
|
return "\n".join(summary_parts[-5:]) # 最多返回 5 行
|
|
|
|
# 如果没有找到关键信息,返回最后一行
|
|
return self._output_lines[-1] if self._output_lines else "执行完成"
|
|
|
|
@property
|
|
def exit_code(self) -> Optional[int]:
|
|
"""获取退出码"""
|
|
return self._exit_code
|
|
|
|
@property
|
|
def output(self) -> str:
|
|
"""获取完整输出"""
|
|
return "\n".join(self._output_lines)
|