autool/utils_ios/wda_health.py
2026-06-17 19:44:18 +08:00

227 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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