2025-10-21 15:43:02 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
2025-11-24 20:38:50 +08:00
|
|
|
|
import time
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
from typing import Optional, Callable
|
2025-11-27 14:35:58 +08:00
|
|
|
|
|
2025-10-21 15:43:02 +08:00
|
|
|
|
from Entity.Variables import WdaAppBundleId
|
|
|
|
|
|
|
2025-11-27 14:35:58 +08:00
|
|
|
|
|
2025-10-21 15:43:02 +08:00
|
|
|
|
class IOSActivator:
|
|
|
|
|
|
"""
|
2025-11-24 20:38:50 +08:00
|
|
|
|
给 iOS17+ 用的 go-ios 激活器(单例):
|
|
|
|
|
|
- 维护一条全局 tunnel 进程
|
2025-11-27 14:35:58 +08:00
|
|
|
|
- 流程:tunnel start -> pair(可多次重试) -> image auto(非致命) -> runwda(多次重试+日志判定成功)
|
2025-11-24 20:38:50 +08:00
|
|
|
|
- WDA 启动成功后触发回调 on_wda_ready(udid)
|
2025-10-21 15:43:02 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# ===== 单例 & 全局 tunnel =====
|
|
|
|
|
|
_instance = None
|
|
|
|
|
|
_instance_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
_tunnel_proc: Optional[subprocess.Popen] = None
|
|
|
|
|
|
_tunnel_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
|
|
|
with cls._instance_lock:
|
|
|
|
|
|
if cls._instance is None:
|
|
|
|
|
|
cls._instance = super().__new__(cls)
|
|
|
|
|
|
return cls._instance
|
|
|
|
|
|
|
2025-11-21 22:03:35 +08:00
|
|
|
|
def __init__(
|
2025-11-27 14:35:58 +08:00
|
|
|
|
self,
|
|
|
|
|
|
ios_path: Optional[str] = None,
|
|
|
|
|
|
pair_timeout: int = 60, # 配对最多等多久
|
|
|
|
|
|
pair_retry_interval: int = 3, # 配对重试间隔
|
|
|
|
|
|
runwda_max_retry: int = 10, # runwda 最大重试次数
|
|
|
|
|
|
runwda_retry_interval: int = 3, # runwda 重试间隔
|
|
|
|
|
|
runwda_wait_timeout: int = 25 # 单次 runwda 等待“成功日志”的超时时间
|
2025-11-21 22:03:35 +08:00
|
|
|
|
):
|
2025-11-24 20:38:50 +08:00
|
|
|
|
if getattr(self, "_inited", False):
|
|
|
|
|
|
return
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# 运行路径处理(源码 / Nuitka EXE)
|
2025-11-21 22:03:35 +08:00
|
|
|
|
if "__compiled__" in globals():
|
2025-11-24 20:38:50 +08:00
|
|
|
|
base_dir = os.path.dirname(sys.executable)
|
2025-11-21 22:03:35 +08:00
|
|
|
|
else:
|
2025-11-24 20:38:50 +08:00
|
|
|
|
cur_file = os.path.abspath(__file__)
|
|
|
|
|
|
base_dir = os.path.dirname(os.path.dirname(cur_file))
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
|
|
|
|
|
resource_dir = os.path.join(base_dir, "resources")
|
2025-11-24 20:38:50 +08:00
|
|
|
|
if not ios_path:
|
2025-11-21 22:03:35 +08:00
|
|
|
|
ios_path = os.path.join(resource_dir, "ios.exe")
|
|
|
|
|
|
|
|
|
|
|
|
self.ios_path = ios_path
|
|
|
|
|
|
self.pair_timeout = pair_timeout
|
|
|
|
|
|
self.pair_retry_interval = pair_retry_interval
|
2025-11-24 20:38:50 +08:00
|
|
|
|
self.runwda_max_retry = runwda_max_retry
|
|
|
|
|
|
self.runwda_retry_interval = runwda_retry_interval
|
|
|
|
|
|
self.runwda_wait_timeout = runwda_wait_timeout
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
self._lock = threading.Lock()
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# ========= 关键:这里改成“真正隐藏窗口”的安全版 =========
|
2025-11-21 22:03:35 +08:00
|
|
|
|
self._creationflags = 0
|
2025-11-24 20:38:50 +08:00
|
|
|
|
self._startupinfo = None
|
2025-10-28 15:09:36 +08:00
|
|
|
|
if os.name == "nt":
|
2025-10-24 22:04:28 +08:00
|
|
|
|
try:
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# 只用 CREATE_NO_WINDOW,不搞 DETACHED_PROCESS / NEW_PROCESS_GROUP 之类的骚操作
|
2025-11-21 22:03:35 +08:00
|
|
|
|
self._creationflags = subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]
|
2025-10-24 22:04:28 +08:00
|
|
|
|
except Exception:
|
2025-11-21 22:03:35 +08:00
|
|
|
|
self._creationflags = 0
|
2025-11-27 14:35:58 +08:00
|
|
|
|
|
2025-11-21 22:03:35 +08:00
|
|
|
|
si = subprocess.STARTUPINFO()
|
2025-11-27 14:35:58 +08:00
|
|
|
|
try:
|
|
|
|
|
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined]
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# 某些极端环境下可能没有 STARTF_USESHOWWINDOW,忽略即可
|
|
|
|
|
|
pass
|
2025-11-21 22:03:35 +08:00
|
|
|
|
si.wShowWindow = 0 # SW_HIDE
|
|
|
|
|
|
self._startupinfo = si
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# ========= 关键部分结束 =========
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
self._inited = True
|
|
|
|
|
|
|
|
|
|
|
|
# ===== 通用同步命令执行 =====
|
2025-11-21 22:03:35 +08:00
|
|
|
|
def _run(
|
|
|
|
|
|
self,
|
|
|
|
|
|
args,
|
|
|
|
|
|
desc: str = "",
|
|
|
|
|
|
timeout: Optional[int] = None,
|
|
|
|
|
|
check: bool = True,
|
2025-11-24 20:38:50 +08:00
|
|
|
|
):
|
2025-11-21 22:03:35 +08:00
|
|
|
|
cmd = [self.ios_path] + list(args)
|
2025-11-24 20:38:50 +08:00
|
|
|
|
cmd_str = " ".join(cmd)
|
|
|
|
|
|
if desc:
|
|
|
|
|
|
print(f"[ios] 执行命令({desc}): {cmd_str}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[ios] 执行命令: {cmd_str}")
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-21 22:03:35 +08:00
|
|
|
|
try:
|
|
|
|
|
|
proc = subprocess.run(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
|
|
|
|
|
timeout=timeout,
|
|
|
|
|
|
creationflags=self._creationflags,
|
2025-11-24 20:38:50 +08:00
|
|
|
|
startupinfo=self._startupinfo,
|
2025-11-21 22:03:35 +08:00
|
|
|
|
)
|
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
|
if check:
|
|
|
|
|
|
raise
|
|
|
|
|
|
return -1, "", "timeout"
|
|
|
|
|
|
|
|
|
|
|
|
out = proc.stdout or ""
|
|
|
|
|
|
err = proc.stderr or ""
|
|
|
|
|
|
if check and proc.returncode != 0:
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[ios] 命令失败({desc}), rc={proc.returncode}")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
raise RuntimeError(f"[ios] 命令失败({desc}), returncode={proc.returncode}")
|
|
|
|
|
|
|
|
|
|
|
|
return proc.returncode, out, err
|
|
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# ===== tunnel 相关 =====
|
|
|
|
|
|
def _drain_process_output(self, proc: subprocess.Popen, name: str):
|
|
|
|
|
|
"""吃掉后台进程输出,防止缓冲区阻塞"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if proc.stdout:
|
|
|
|
|
|
for line in proc.stdout:
|
|
|
|
|
|
line = line.rstrip()
|
|
|
|
|
|
if line:
|
|
|
|
|
|
print(f"[ios][{name}] {line}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[ios][{name}] 读取 stdout 异常: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
if proc.stderr:
|
|
|
|
|
|
for line in proc.stderr:
|
|
|
|
|
|
line = line.rstrip()
|
|
|
|
|
|
if line:
|
|
|
|
|
|
print(f"[ios][{name}][stderr] {line}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[ios][{name}] 读取 stderr 异常: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def _spawn_tunnel(self):
|
2025-11-27 14:35:58 +08:00
|
|
|
|
"""启动 / 复用全局 tunnel(不隐藏窗口)"""
|
2025-11-24 20:38:50 +08:00
|
|
|
|
with IOSActivator._tunnel_lock:
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# 已有并且还在跑就复用
|
2025-11-24 20:38:50 +08:00
|
|
|
|
if IOSActivator._tunnel_proc is not None and IOSActivator._tunnel_proc.poll() is None:
|
2025-11-21 22:03:35 +08:00
|
|
|
|
print("[ios] tunnel 已经在运行,跳过重新启动")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
cmd = [self.ios_path, "tunnel", "start"]
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print("[ios] 启动 go-ios tunnel:", " ".join(cmd))
|
2025-10-24 22:04:28 +08:00
|
|
|
|
try:
|
2025-11-21 22:03:35 +08:00
|
|
|
|
proc = subprocess.Popen(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
2025-11-27 14:35:58 +08:00
|
|
|
|
creationflags=self._creationflags, # 0:不隐藏
|
|
|
|
|
|
startupinfo=self._startupinfo, # None:不隐藏
|
2025-10-28 15:09:36 +08:00
|
|
|
|
)
|
2025-10-24 22:04:28 +08:00
|
|
|
|
except Exception as e:
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print("[ios] 启动 tunnel 失败(忽略):", e)
|
2025-11-21 22:03:35 +08:00
|
|
|
|
return
|
2025-10-27 21:44:16 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
IOSActivator._tunnel_proc = proc
|
|
|
|
|
|
print("[ios] tunnel 启动成功, PID=", proc.pid)
|
2025-10-27 21:44:16 +08:00
|
|
|
|
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# 后台吃日志
|
2025-11-21 22:03:35 +08:00
|
|
|
|
threading.Thread(
|
|
|
|
|
|
target=self._drain_process_output,
|
|
|
|
|
|
args=(proc, "tunnel"),
|
|
|
|
|
|
daemon=True,
|
|
|
|
|
|
).start()
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# ===== pair & image =====
|
|
|
|
|
|
def _pair_until_success(self, udid: str):
|
2025-11-21 22:03:35 +08:00
|
|
|
|
deadline = time.time() + self.pair_timeout
|
|
|
|
|
|
attempt = 0
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
attempt += 1
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[ios] 开始配对设备({udid}),第 {attempt} 次尝试")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
|
|
|
|
|
rc, out, err = self._run(
|
|
|
|
|
|
["--udid", udid, "pair"],
|
|
|
|
|
|
desc=f"pair({udid})",
|
|
|
|
|
|
timeout=20,
|
|
|
|
|
|
check=False,
|
|
|
|
|
|
)
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-21 22:03:35 +08:00
|
|
|
|
text = (out or "") + "\n" + (err or "")
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# 打印一份完整输出,方便调试
|
|
|
|
|
|
if text.strip():
|
|
|
|
|
|
print("[ios][pair] output:\n", text.strip())
|
|
|
|
|
|
|
2025-11-21 22:03:35 +08:00
|
|
|
|
if "Successfully paired" in text:
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[ios] 设备 {udid} 配对成功")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
return
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-21 22:03:35 +08:00
|
|
|
|
if time.time() >= deadline:
|
2025-11-24 20:38:50 +08:00
|
|
|
|
raise RuntimeError(f"[ios] 设备 {udid} 在超时时间内配对失败(rc={rc})")
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-21 22:03:35 +08:00
|
|
|
|
time.sleep(self.pair_retry_interval)
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
def _mount_dev_image(self, udid: str):
|
|
|
|
|
|
print(f"[ios] 开始为设备 {udid} 挂载开发者镜像 (image auto)")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
rc, out, err = self._run(
|
|
|
|
|
|
["--udid", udid, "image", "auto"],
|
|
|
|
|
|
desc=f"image auto({udid})",
|
|
|
|
|
|
timeout=300,
|
|
|
|
|
|
check=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
text = (out or "") + "\n" + (err or "")
|
|
|
|
|
|
text_lower = text.lower()
|
|
|
|
|
|
success_keywords = [
|
|
|
|
|
|
"success mounting image",
|
|
|
|
|
|
"there is already a developer image mounted",
|
2025-10-24 22:04:28 +08:00
|
|
|
|
]
|
2025-11-21 22:03:35 +08:00
|
|
|
|
if any(k in text_lower for k in success_keywords):
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[ios] 设备 {udid} 开发者镜像挂载完成")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
if text.strip():
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print("[ios][image auto] output:\n", text.strip())
|
2025-10-24 22:04:28 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[ios] 设备 {udid} 挂载开发者镜像可能失败(rc={rc}),输出:\n{text.strip()}")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# ===== runwda(关键逻辑) =====
|
|
|
|
|
|
def _run_wda_once_async(self, udid: str, on_wda_ready: Optional[Callable[[str], None]]) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
单次 runwda:
|
|
|
|
|
|
- 异步启动 ios.exe
|
|
|
|
|
|
- 实时读 stdout/stderr
|
|
|
|
|
|
- 捕获关键日志(got capabilities / authorized true)视为成功
|
|
|
|
|
|
- 超时/进程退出且未成功 -> 失败
|
|
|
|
|
|
"""
|
|
|
|
|
|
cmd = [
|
|
|
|
|
|
self.ios_path,
|
2025-11-21 22:03:35 +08:00
|
|
|
|
f"--udid={udid}",
|
|
|
|
|
|
"runwda",
|
|
|
|
|
|
f"--bundleid={WdaAppBundleId}",
|
|
|
|
|
|
f"--testrunnerbundleid={WdaAppBundleId}",
|
2025-11-24 20:38:50 +08:00
|
|
|
|
"--xctestconfig=yolo.xctest",
|
2025-10-24 22:04:28 +08:00
|
|
|
|
]
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print("[ios] 异步启动 runwda:", " ".join(cmd))
|
2025-10-24 22:04:28 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
try:
|
|
|
|
|
|
proc = subprocess.Popen(
|
|
|
|
|
|
cmd,
|
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
|
text=True,
|
2025-11-27 14:35:58 +08:00
|
|
|
|
creationflags=self._creationflags, # 0:不隐藏
|
2025-11-24 20:38:50 +08:00
|
|
|
|
startupinfo=self._startupinfo,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[ios] 启动 runwda 进程失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
success_evt = threading.Event()
|
|
|
|
|
|
|
|
|
|
|
|
def _reader(pipe, tag: str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
for raw in pipe:
|
|
|
|
|
|
line = (raw or "").rstrip()
|
|
|
|
|
|
if not line:
|
|
|
|
|
|
continue
|
|
|
|
|
|
print(f"[WDA-LOG] {line}")
|
|
|
|
|
|
lower = line.lower()
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# 你实测的“成功特征”
|
2025-11-24 20:38:50 +08:00
|
|
|
|
if "got capabilities" in lower or '"authorized":true' in lower:
|
|
|
|
|
|
success_evt.set()
|
|
|
|
|
|
print(f"[ios] 捕获到 WDA 启动成功日志({tag}),udid={udid}")
|
|
|
|
|
|
break
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[ios] 读取 {tag} 日志异常: {e}")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# 日志线程
|
|
|
|
|
|
if proc.stdout:
|
|
|
|
|
|
threading.Thread(target=_reader, args=(proc.stdout, "stdout"), daemon=True).start()
|
|
|
|
|
|
if proc.stderr:
|
|
|
|
|
|
threading.Thread(target=_reader, args=(proc.stderr, "stderr"), daemon=True).start()
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# 等待成功 / 退出 / 超时
|
|
|
|
|
|
start = time.time()
|
|
|
|
|
|
while True:
|
|
|
|
|
|
if success_evt.is_set():
|
|
|
|
|
|
print(f"[ios] WDA 日志确认已启动,udid={udid}")
|
|
|
|
|
|
if on_wda_ready:
|
|
|
|
|
|
try:
|
|
|
|
|
|
on_wda_ready(udid)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[WDA] 回调执行异常: {e}")
|
|
|
|
|
|
# 不主动杀进程,让 WDA 挂在那儿
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
rc = proc.poll()
|
|
|
|
|
|
if rc is not None:
|
|
|
|
|
|
print(f"[ios] runwda 进程退出 rc={rc},未检测到成功日志,udid={udid}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
if time.time() - start > self.runwda_wait_timeout:
|
|
|
|
|
|
print(f"[ios] runwda 等待超时({self.runwda_wait_timeout}s),未确认成功,udid={udid}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
proc.terminate()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
|
|
|
|
def _run_wda_with_retry(self, udid: str, on_wda_ready: Optional[Callable[[str], None]]) -> bool:
|
|
|
|
|
|
for attempt in range(1, self.runwda_max_retry + 1):
|
|
|
|
|
|
print(f"[ios] runwda 尝试 {attempt}/{self.runwda_max_retry},udid={udid}")
|
|
|
|
|
|
ok = self._run_wda_once_async(udid, on_wda_ready)
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
print(f"[ios] runwda 第 {attempt} 次尝试成功,udid={udid}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[ios] runwda 第 {attempt} 次尝试失败,udid={udid}")
|
|
|
|
|
|
if attempt < self.runwda_max_retry:
|
|
|
|
|
|
time.sleep(self.runwda_retry_interval)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"[ios] runwda 多次失败,放弃,udid={udid}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# ===== 对外主流程 =====
|
|
|
|
|
|
def activate_ios17(self, udid: str, on_wda_ready: Optional[Callable[[str], None]] = None) -> None:
|
|
|
|
|
|
print(f"[WDA] iOS17+ 激活开始,udid={udid}, 回调={on_wda_ready}")
|
|
|
|
|
|
|
2025-11-27 14:35:58 +08:00
|
|
|
|
# 1. 先确保 tunnel 在跑
|
2025-11-21 22:03:35 +08:00
|
|
|
|
self._spawn_tunnel()
|
|
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# 2. 配对
|
2025-10-23 18:53:22 +08:00
|
|
|
|
try:
|
2025-11-21 22:03:35 +08:00
|
|
|
|
self._pair_until_success(udid)
|
|
|
|
|
|
except Exception as e:
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[WDA] pair 失败,终止激活流程 udid={udid}, err={e}")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
return
|
2025-10-23 18:53:22 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# 3. 挂镜像(非致命)
|
2025-11-21 22:03:35 +08:00
|
|
|
|
try:
|
|
|
|
|
|
self._mount_dev_image(udid)
|
|
|
|
|
|
except Exception as e:
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[WDA] 挂载开发者镜像异常(忽略) udid={udid}, err={e}")
|
2025-10-23 18:53:22 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
# 4. runwda + 回调
|
|
|
|
|
|
ok = self._run_wda_with_retry(udid, on_wda_ready)
|
|
|
|
|
|
if not ok:
|
|
|
|
|
|
print(f"[WDA] runwda 多次失败,可能需要手动检查设备,udid={udid}")
|
2025-11-21 22:03:35 +08:00
|
|
|
|
|
2025-11-24 20:38:50 +08:00
|
|
|
|
print(f"[WDA] iOS17+ 激活流程结束(不代表一定成功),udid={udid}")
|