# -*- coding: utf-8 -*- """ WDA 健康监控与异步恢复模块 当 WDA 卡死时,在后台线程异步重启 WDA 进程。 IOSDevice 中已有的 _wait_wda_ready() 调用会自然等待 WDA 恢复。 多次恢复失败后通知调用方放弃当前测试。 """ import logging import threading import time logger = logging.getLogger("WDAHealthMonitor") class WDAHealthMonitor: """WDA 健康监控与异步恢复 职责: 1. 接收来自 IOSDevice 的恢复请求 2. 在后台线程执行 stop_wda → start_wda → 验证就绪 3. 跟踪连续恢复次数,超出上限时通知放弃 """ MAX_RECOVER_ATTEMPTS = 5 # 最大连续恢复次数 def __init__(self, wda_url: str, go_ios_runner=None): """ :param wda_url: WDA 服务地址 (如 http://localhost:8100) :param go_ios_runner: GoIOSRunner 实例,用于 stop/start WDA 进程 """ self.wda_url = wda_url self.runner = go_ios_runner self._recovering = False # 是否正在恢复中 self._recovery_lock = threading.Lock() self._recover_count = 0 # 连续恢复次数(成功操作后重置) self._last_recovery_success = None # 上次恢复结果 @property def is_recovering(self) -> bool: """WDA 是否正在恢复中""" return self._recovering def has_exceeded_max_attempts(self) -> bool: """是否超过最大恢复次数""" return self._recover_count >= self.MAX_RECOVER_ATTEMPTS def reset_attempts(self): """重置恢复次数""" self._recover_count = 0 def reset_recover_count(self): """WDA 操作成功后调用,重置连续恢复计数""" if self._recover_count > 0: logger.info(f"WDA 操作成功,重置恢复计数 (之前: {self._recover_count})") self._recover_count = 0 def check_health(self, timeout: float = 5) -> bool: """通过 WDA Client 的 status() 检查 WDA 是否存活 支持 http: HTTP 连接和 UDID USB 连接两种方式。 status() 返回示例: {'ready': True, 'state': 'success', 'message': 'WebDriverAgent is ready...', ...} :param timeout: 连接/请求超时时间(秒) :return: True 表示 WDA 正常 """ try: # 根据 wda_url 格式选择连接方式 import sys from pathlib import Path _wda_dir = str(Path(__file__).resolve().parent.parent / "DroidBot" / "platforms" / "ios") if _wda_dir not in sys.path: sys.path.insert(0, _wda_dir) from wda import Client as WDAClient, USBClient as WDAUSBClient if self.wda_url and self.wda_url.startswith("http:"): client = WDAClient(self.wda_url) else: client = WDAUSBClient(udid=self.wda_url or "") status = client.status() # status() 直接返回 dict,其中 ready 字段表示 WDA 是否就绪 return bool(status.get("ready", False)) except Exception: pass return False def trigger_recovery(self) -> bool: """触发异步 WDA 恢复(非阻塞) 在后台线程执行 stop_wda → start_wda → 等待就绪。 如果已经在恢复中则跳过。 :return: True 表示已触发恢复,False 表示已在恢复中或缺少 runner """ if not self.runner: logger.error("未设置 GoIOSRunner,无法执行 WDA 恢复") return False with self._recovery_lock: if self._recovering: logger.info("WDA 恢复正在进行中,跳过重复触发") return False self._recovering = True self._recover_count += 1 logger.warning(f"触发 WDA 异步恢复 (第 {self._recover_count}/{self.MAX_RECOVER_ATTEMPTS} 次)") # 启动后台恢复线程 recovery_thread = threading.Thread( target=self._do_recovery, name="WDA-Recovery", daemon=True ) recovery_thread.start() return True def trigger_recovery_and_wait(self, timeout: float = 120) -> bool: """触发 WDA 恢复并同步阻塞等待完成(供底层调用方原地等待) 如果已在恢复中则直接等待当前恢复完成;否则先触发恢复再等待。 超出 MAX_RECOVER_ATTEMPTS 时不再触发恢复,直接返回 False。 :param timeout: 最长等待时间(秒) :return: True 表示 WDA 已就绪,False 表示恢复失败或超次数 """ if self.has_exceeded_max_attempts(): logger.error(f"WDA 恢复次数已达上限 ({self.MAX_RECOVER_ATTEMPTS}),放弃等待") return False if not self._recovering: # 未在恢复中,触发新的恢复 triggered = self.trigger_recovery() if not triggered and not self._recovering: # trigger_recovery 返回 False 且并非因为已在恢复中,说明缺少 runner return False # 等待恢复完成 logger.info(f"等待 WDA 恢复完成(最长 {timeout}s)...") deadline = time.time() + timeout while time.time() < deadline: if not self._recovering: # 恢复线程已结束,检查实际健康状态 is_ready = self.check_health(timeout=3) if is_ready: logger.info("WDA 已恢复并就绪") else: logger.error("WDA 恢复完成但健康检查失败") return is_ready time.sleep(1) logger.error(f"等待 WDA 恢复超时 ({timeout}s)") return False def _do_recovery(self): """后台线程:执行 WDA 重启恢复""" try: # 1. 停止 WDA logger.info("[恢复] 正在停止 WDA...") try: self.runner.stop_wda(timeout=10) except Exception as e: logger.warning(f"[恢复] 停止 WDA 时异常 (继续): {e}") time.sleep(2) # 等待端口释放 # 2. 重启端口转发(WDA 重启后旧的转发连接会断开) logger.info("[恢复] 正在重启端口转发...") if hasattr(self.runner, 'restart_port_forward'): if not self.runner.restart_port_forward(port=8100): logger.warning("[恢复] 端口转发重启失败,继续尝试启动 WDA...") time.sleep(1) # 等待端口转发就绪 # 3. 重新启动 WDA logger.info("[恢复] 正在重启 WDA...") try: wda_process = self.runner.start_wda(port=8100) if wda_process and wda_process.poll() is None: logger.info(f"[恢复] WDA 进程已启动 (PID: {wda_process.pid})") else: logger.error("[恢复] WDA 进程启动失败") self._last_recovery_success = False return except Exception as e: logger.error(f"[恢复] 启动 WDA 异常: {e}") self._last_recovery_success = False return # 3. 等待 WDA 就绪 logger.info("[恢复] 等待 WDA 就绪...") ready = self._wait_for_ready(timeout=60) if ready: logger.info("[恢复] WDA 恢复成功!") self._last_recovery_success = True else: logger.error("[恢复] WDA 重启后未就绪") self._last_recovery_success = False except Exception as e: logger.error(f"[恢复] 恢复过程异常: {e}") import traceback traceback.print_exc() self._last_recovery_success = False finally: with self._recovery_lock: self._recovering = False def _wait_for_ready(self, timeout: float = 30) -> bool: """等待 WDA 就绪 :param timeout: 最大等待时间(秒) :return: WDA 是否就绪 """ deadline = time.time() + timeout while time.time() < deadline: if self.check_health(timeout=3): return True time.sleep(1) return False