227 lines
8.6 KiB
Python
227 lines
8.6 KiB
Python
# -*- 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
|