🐛 修復桌面應用測試啟動問題

This commit is contained in:
Minidoracat 2025-06-11 06:29:42 +08:00
parent 0e04186805
commit d5ba76e248
3 changed files with 278 additions and 53 deletions

View File

@ -91,7 +91,18 @@ def run_tests(args):
# 在 Windows 上抑制 asyncio 警告 # 在 Windows 上抑制 asyncio 警告
if sys.platform == "win32": if sys.platform == "win32":
os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning" import warnings
# 設置更全面的警告抑制
os.environ["PYTHONWARNINGS"] = (
"ignore::ResourceWarning,ignore::DeprecationWarning"
)
warnings.filterwarnings("ignore", category=ResourceWarning)
warnings.filterwarnings("ignore", message=".*unclosed transport.*")
warnings.filterwarnings("ignore", message=".*I/O operation on closed pipe.*")
warnings.filterwarnings("ignore", message=".*unclosed.*")
# 抑制 asyncio 相關的所有警告
warnings.filterwarnings("ignore", module="asyncio.*")
if args.web: if args.web:
print("🧪 執行 Web UI 測試...") print("🧪 執行 Web UI 測試...")
@ -216,7 +227,9 @@ def test_desktop_app():
print("✅ 桌面環境檢查通過") print("✅ 桌面環境檢查通過")
# 設置桌面模式 # 設置測試環境變數,避免端口衝突和權限問題
os.environ["MCP_TEST_MODE"] = "true"
os.environ["MCP_WEB_PORT"] = "9767" # 使用不同端口避免與 Web 測試衝突
os.environ["MCP_FEEDBACK_MODE"] = "desktop" os.environ["MCP_FEEDBACK_MODE"] = "desktop"
print("🔧 創建 Electron 管理器...") print("🔧 創建 Electron 管理器...")
@ -254,6 +267,11 @@ def test_desktop_app():
traceback.print_exc() traceback.print_exc()
return False return False
finally:
# 清理測試環境變數
os.environ.pop("MCP_TEST_MODE", None)
os.environ.pop("MCP_WEB_PORT", None)
os.environ.pop("MCP_FEEDBACK_MODE", None)
async def wait_for_process(process): async def wait_for_process(process):

View File

@ -120,19 +120,33 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di
if desktop_success: if desktop_success:
debug_log("桌面應用啟動成功,等待用戶回饋...") debug_log("桌面應用啟動成功,等待用戶回饋...")
# 等待用戶回饋 try:
result = await session.wait_for_feedback(timeout) # 等待用戶回饋
debug_log("收到桌面應用用戶回饋") result = await session.wait_for_feedback(timeout)
return result debug_log("收到桌面應用用戶回饋")
debug_log("桌面應用啟動失敗,回退到 Web 模式") return result
# 回退到 Web 模式 finally:
from ..web import launch_web_feedback_ui # 確保 Electron 進程被正確清理
debug_log("清理 Electron 進程...")
await manager.cleanup_async()
debug_log("Electron 進程清理完成")
else:
debug_log("桌面應用啟動失敗,回退到 Web 模式")
# 回退到 Web 模式
from ..web import launch_web_feedback_ui
return await launch_web_feedback_ui(project_dir, summary, timeout) return await launch_web_feedback_ui(project_dir, summary, timeout)
except Exception as e: except Exception as e:
debug_log(f"桌面應用啟動過程中出錯: {e}") debug_log(f"桌面應用啟動過程中出錯: {e}")
debug_log("回退到 Web 模式") debug_log("回退到 Web 模式")
# 確保清理 Electron 進程
try:
if "manager" in locals():
await manager.cleanup_async()
except Exception as cleanup_error:
debug_log(f"清理 Electron 進程時出錯: {cleanup_error}")
# 回退到 Web 模式 # 回退到 Web 模式
from ..web import launch_web_feedback_ui from ..web import launch_web_feedback_ui

View File

@ -16,24 +16,46 @@ Electron 管理器
""" """
import asyncio import asyncio
import os
import platform
import signal
import subprocess import subprocess
import warnings
from pathlib import Path from pathlib import Path
from ..debug import web_debug_log as debug_log from ..debug import web_debug_log as debug_log
from ..utils.error_handler import ErrorHandler, ErrorType from ..utils.error_handler import ErrorHandler, ErrorType
def suppress_windows_asyncio_warnings():
"""抑制 Windows 上的 asyncio 相關警告"""
if platform.system().lower() == "windows":
# 抑制 ResourceWarning 和 asyncio 相關警告
warnings.filterwarnings("ignore", category=ResourceWarning)
warnings.filterwarnings("ignore", message=".*unclosed transport.*")
warnings.filterwarnings("ignore", message=".*I/O operation on closed pipe.*")
# 設置環境變數抑制 asyncio 警告
os.environ.setdefault("PYTHONWARNINGS", "ignore::ResourceWarning")
debug_log("已設置 Windows asyncio 警告抑制")
class ElectronManager: class ElectronManager:
"""Electron 進程管理器""" """Electron 進程管理器 - 跨平台支持"""
def __init__(self): def __init__(self):
"""初始化 Electron 管理器""" """初始化 Electron 管理器"""
self.electron_process: asyncio.subprocess.Process | None = None self.electron_process: asyncio.subprocess.Process | None = None
self.desktop_dir = Path(__file__).parent self.desktop_dir = Path(__file__).parent
self.web_server_port: int | None = None self.web_server_port: int | None = None
self.platform = platform.system().lower()
self._cleanup_in_progress = False
# 設置平台特定的警告抑制
suppress_windows_asyncio_warnings()
debug_log("ElectronManager 初始化完成") debug_log("ElectronManager 初始化完成")
debug_log(f"桌面模組目錄: {self.desktop_dir}") debug_log(f"桌面模組目錄: {self.desktop_dir}")
debug_log(f"檢測到平台: {self.platform}")
async def launch_desktop_app(self, summary: str, project_dir: str) -> bool: async def launch_desktop_app(self, summary: str, project_dir: str) -> bool:
""" """
@ -142,50 +164,221 @@ class ElectronManager:
return False return False
def cleanup(self): def cleanup(self):
"""清理資源""" """同步清理資源(向後兼容)"""
if self.electron_process: try:
try: # 在事件循環中運行異步清理
# 檢查進程是否還在運行 loop = asyncio.get_event_loop()
if self.electron_process.returncode is None: if loop.is_running():
self.electron_process.terminate() # 如果事件循環正在運行,創建任務
debug_log("Electron 進程已終止") asyncio.create_task(self.cleanup_async())
else: else:
debug_log("Electron 進程已自然結束") # 如果沒有事件循環,運行異步清理
except Exception as e: asyncio.run(self.cleanup_async())
debug_log(f"終止 Electron 進程時出錯: {e}") except Exception as e:
try: debug_log(f"同步清理失敗,嘗試基本清理: {e}")
if self.electron_process.returncode is None: self._basic_cleanup()
self.electron_process.kill()
debug_log("強制終止 Electron 進程")
except Exception as kill_error:
debug_log(f"強制終止 Electron 進程失敗: {kill_error}")
finally:
# 關閉管道以避免 ResourceWarning
try:
# 對於 asyncio 子進程,需要特殊處理
if (
hasattr(self.electron_process, "stdout")
and self.electron_process.stdout
):
if hasattr(self.electron_process.stdout, "close"):
self.electron_process.stdout.close()
if (
hasattr(self.electron_process, "stderr")
and self.electron_process.stderr
):
if hasattr(self.electron_process.stderr, "close"):
self.electron_process.stderr.close()
if (
hasattr(self.electron_process, "stdin")
and self.electron_process.stdin
):
if hasattr(self.electron_process.stdin, "close"):
self.electron_process.stdin.close()
except Exception:
# 忽略管道關閉錯誤,這些通常是無害的
pass
async def cleanup_async(self):
"""異步清理資源 - 跨平台支持"""
if self._cleanup_in_progress or not self.electron_process:
return
self._cleanup_in_progress = True
debug_log(f"開始清理 Electron 進程 (平台: {self.platform})")
try:
# 檢查進程是否還在運行
if self.electron_process.returncode is None:
await self._terminate_process_cross_platform()
else:
debug_log("Electron 進程已自然結束")
# 等待進程完全結束並清理管道
await self._wait_and_cleanup_pipes()
except Exception as e:
debug_log(f"清理 Electron 進程時出錯: {e}")
# 嘗試強制清理
await self._force_cleanup()
finally:
self.electron_process = None
self._cleanup_in_progress = False
debug_log("Electron 進程清理完成")
async def _terminate_process_cross_platform(self):
"""跨平台進程終止"""
if self.platform == "windows":
await self._terminate_windows()
else:
await self._terminate_unix()
async def _terminate_windows(self):
"""Windows 平台進程終止"""
debug_log("使用 Windows 進程終止策略")
if not self.electron_process:
return
try:
# Windows: 使用 terminate() 然後等待
self.electron_process.terminate()
try:
await asyncio.wait_for(self.electron_process.wait(), timeout=5.0)
debug_log("Electron 進程已優雅終止")
except TimeoutError:
debug_log("優雅終止超時,強制終止")
self.electron_process.kill()
await asyncio.wait_for(self.electron_process.wait(), timeout=3.0)
debug_log("Electron 進程已強制終止")
except Exception as e:
debug_log(f"Windows 進程終止失敗: {e}")
raise
async def _terminate_unix(self):
"""Unix 系統進程終止"""
debug_log("使用 Unix 進程終止策略")
if not self.electron_process:
return
try:
# Unix: 發送 SIGTERM 然後等待
self.electron_process.send_signal(signal.SIGTERM)
try:
await asyncio.wait_for(self.electron_process.wait(), timeout=5.0)
debug_log("Electron 進程已優雅終止")
except TimeoutError:
debug_log("優雅終止超時,發送 SIGKILL")
# 使用 getattr 來處理可能不存在的 SIGKILL
sigkill = getattr(signal, "SIGKILL", signal.SIGTERM)
self.electron_process.send_signal(sigkill)
await asyncio.wait_for(self.electron_process.wait(), timeout=3.0)
debug_log("Electron 進程已強制終止")
except Exception as e:
debug_log(f"Unix 進程終止失敗: {e}")
raise
async def _wait_and_cleanup_pipes(self):
"""等待進程結束並清理管道"""
if not self.electron_process:
return
try:
# 確保進程已經結束
if self.electron_process.returncode is None:
await self.electron_process.wait()
# 平台特定的管道清理
if self.platform == "windows":
await self._cleanup_pipes_windows()
else:
await self._cleanup_pipes_unix()
except Exception as e:
debug_log(f"管道清理失敗: {e}")
async def _cleanup_pipes_windows(self):
"""Windows 平台管道清理"""
debug_log("清理 Windows 管道")
if not self.electron_process:
return
try:
# Windows: 需要特殊處理 asyncio 管道
pipes = [
("stdout", self.electron_process.stdout),
("stderr", self.electron_process.stderr),
("stdin", self.electron_process.stdin),
]
for pipe_name, pipe in pipes:
if pipe and hasattr(pipe, "close"):
try:
# 檢查是否有 is_closing 方法(某些管道類型可能沒有)
if not hasattr(pipe, "is_closing") or not pipe.is_closing():
pipe.close()
# 等待管道關閉
if hasattr(pipe, "wait_closed"):
await pipe.wait_closed()
debug_log(f"已關閉 {pipe_name} 管道")
except Exception as e:
# Windows 上的管道關閉錯誤通常是無害的
debug_log(f"關閉 {pipe_name} 管道時出現預期錯誤: {e}")
except Exception as e:
debug_log(f"Windows 管道清理失敗: {e}")
async def _cleanup_pipes_unix(self):
"""Unix 系統管道清理"""
debug_log("清理 Unix 管道")
if not self.electron_process:
return
try:
# Unix: 直接關閉管道
pipes = [
("stdout", self.electron_process.stdout),
("stderr", self.electron_process.stderr),
("stdin", self.electron_process.stdin),
]
for pipe_name, pipe in pipes:
if pipe and hasattr(pipe, "close"):
try:
pipe.close()
if hasattr(pipe, "wait_closed"):
await pipe.wait_closed()
debug_log(f"已關閉 {pipe_name} 管道")
except Exception as e:
debug_log(f"關閉 {pipe_name} 管道失敗: {e}")
except Exception as e:
debug_log(f"Unix 管道清理失敗: {e}")
async def _force_cleanup(self):
"""強制清理(最後手段)"""
debug_log("執行強制清理")
try:
if self.electron_process and self.electron_process.returncode is None:
# 嘗試使用 psutil 強制終止
try:
import psutil
process = psutil.Process(self.electron_process.pid)
process.kill()
debug_log("使用 psutil 強制終止進程")
except ImportError:
debug_log("psutil 不可用,使用系統調用")
if self.electron_process:
if self.platform == "windows":
self.electron_process.kill()
else:
# 使用 getattr 來處理可能不存在的 SIGKILL
sigkill = getattr(signal, "SIGKILL", signal.SIGTERM)
self.electron_process.send_signal(sigkill)
except Exception as e:
debug_log(f"強制終止失敗: {e}")
# 基本清理
self._basic_cleanup()
except Exception as e:
debug_log(f"強制清理失敗: {e}")
def _basic_cleanup(self):
"""基本清理(同步)"""
debug_log("執行基本清理")
try:
if self.electron_process:
# 嘗試基本的管道關閉
for pipe_name in ["stdout", "stderr", "stdin"]:
pipe = getattr(self.electron_process, pipe_name, None)
if pipe and hasattr(pipe, "close"):
try:
pipe.close()
except Exception:
pass # 忽略錯誤
self.electron_process = None self.electron_process = None
except Exception as e:
debug_log(f"基本清理失敗: {e}")
async def _create_package_json(self): async def _create_package_json(self):
"""創建 package.json 文件""" """創建 package.json 文件"""