From 2ad53eb1db2f0928e66b8d4e94a3bdf184209f6b Mon Sep 17 00:00:00 2001 From: milk <53408947@qq.com> Date: Tue, 28 Oct 2025 15:09:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Module/DeviceInfo.py | 9 +- Module/IOSActivator.py | 230 ++++++++++-------- Module/__pycache__/DeviceInfo.cpython-312.pyc | Bin 27332 -> 27289 bytes Module/__pycache__/Main.cpython-312.pyc | Bin 3738 -> 3738 bytes Utils/__pycache__/LogManager.cpython-312.pyc | Bin 14663 -> 14663 bytes script/ScriptManager.py | 1 + .../__pycache__/ScriptManager.cpython-312.pyc | Bin 68734 -> 68773 bytes 7 files changed, 140 insertions(+), 100 deletions(-) diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index 4c39fae..bbadad2 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -204,11 +204,10 @@ class DeviceInfo: if major > 17: print("进入iOS17设备的分支") print(f"[WDA] iOS>17 调用 IOSActivator (port={wdaScreenPort})") - try: - IOSActivator().activate(udid) - print("wda启动完成") - except Exception as e: - print("错误信息:",e) + print("准备启动隧道") + out = IOSActivator().activate(udid) + print("------------------",out) + print("wda启动完成") else: print(f"[WDA] iOS<=17 启动 WDA app_start (port={wdaScreenPort})") dev = tidevice.Device(udid) diff --git a/Module/IOSActivator.py b/Module/IOSActivator.py index 5c009d3..3769409 100644 --- a/Module/IOSActivator.py +++ b/Module/IOSActivator.py @@ -9,6 +9,7 @@ import subprocess from typing import Optional, List, Tuple, Dict, Set from Entity.Variables import WdaAppBundleId +import time as _t class IOSActivator: @@ -32,139 +33,178 @@ class IOSActivator: self._live_ifaces: Dict[str, Set[str]] = {} # udid -> {iface names} self._registered = False + # =============== 公共入口 =============== def activate( self, udid: str, - wda_bundle_id: str = WdaAppBundleId, - ready_timeout_sec: float = 60.0, + wda_bundle_id: Optional[str] = WdaAppBundleId, + ready_timeout_sec: float = 120.0, + mount_retries: int = 3, + backoff_seconds: float = 2.0, + rsd_probe_retries: int = 5, + rsd_probe_delay_sec: float = 3.0, pre_mount_first: bool = True, - mount_retries: int = 2, - backoff_seconds: float = 1.5, - keep_tunnel: bool = False, - broad_cleanup_on_exit: bool = True, + keep_tunnel: bool = False, # 默认 False:WDA 拉起后关闭隧道 + broad_cleanup_on_exit: bool = True, # 退出时顺带清理所有 pmd3 残留网卡 ) -> str: """ - Windows 简版:不读任何 tunneld 日志,也不做 RSD 解析。 - 逻辑:先探活 -> 开隧道 -> 直接用 HTTP 隧道端口反复尝试启动 WDA -> 探活成功即返回。 + 流程:挂镜像(可选) -> 开隧道 -> 等 RSD -> 启动 WDA + - keep_tunnel=False:WDA 启动后关闭隧道并清理 + - keep_tunnel=True:隧道常驻,由上层/atexit 清理 """ - import time, ctypes, traceback - if not udid or not isinstance(udid, str): raise ValueError("udid is required and must be a non-empty string") - print(f"[activate] UDID={udid}", flush=True) + print(f"[activate] UDID = {udid}") + self._ensure_exit_hooks(broad_cleanup_on_exit=broad_cleanup_on_exit) - # —— 管理员提示(Windows 清理虚拟网卡常用)—— - try: - if ctypes.windll.shell32.IsUserAnAdmin() == 0: - print("[⚠] 未以管理员运行:若需要移除虚拟网卡,可能失败。", flush=True) - except Exception: - pass - - # —— 退出钩子(可选)—— - try: - self._ensure_exit_hooks(broad_cleanup_on_exit=broad_cleanup_on_exit) # type: ignore[attr-defined] - except Exception as e: - print(f"[activate] _ensure_exit_hooks warn: {e}", flush=True) - - # —— 小工具:探活 WDA —— # - def _wda_alive(timeout: float = 2.0) -> bool: + # Windows 管理员检测 + if os.name == "nt": + import ctypes try: - if hasattr(self, "_wda_alive_now"): - return bool(self._wda_alive_now(udid, timeout=timeout)) # type: ignore[attr-defined] - if hasattr(self, "_wda_client"): - cli = self._wda_client(udid) # type: ignore[attr-defined] - if hasattr(cli, "wait_ready"): - return bool(cli.wait_ready(timeout=timeout)) + is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 except Exception: - return False - return False + is_admin = False + if not is_admin: + print("[⚠] 未以管理员运行:若需要移除虚拟网卡,可能失败。") - # 0) 快路径:WDA 已活 - if _wda_alive(2.0): - print("[activate] WDA already alive, skip launching.", flush=True) - return "WDA already alive" + start_ts = _t.time() # 1) 预挂载(失败不致命) - if pre_mount_first and hasattr(self, "_auto_mount_developer_disk"): + if pre_mount_first: try: - self._auto_mount_developer_disk(udid, retries=mount_retries, - backoff_seconds=backoff_seconds) # type: ignore[attr-defined] - time.sleep(1.5) + self._auto_mount_developer_disk( + udid, retries=mount_retries, backoff_seconds=backoff_seconds + ) + _t.sleep(2) except Exception as e: - print(f"[activate] 预挂载失败(继续):{e}", flush=True) + print(f"[activate] 预挂载失败(稍后再试):{e}") - # 2) 开隧道(关键:拿到 HTTP 端口即可;不读取任何 stdout/stderr) - proc = None - http_host, http_port = "127.0.0.1", None - try: - ret = self._start_tunneld(udid) # type: ignore[attr-defined] - if isinstance(ret, tuple): - proc, http_port = ret[0], ret[1] - else: - proc = ret - if http_port is None: - # 若你的 _start_tunneld 固定端口,可在这里写死(例如 8100/某自定义端口) - raise RuntimeError("未获取到 HTTP 隧道端口(_start_tunneld 未返回端口)") - except Exception: - # 即便开隧道失败,也再探活一次(可能本来就活) - if _wda_alive(2.0): - print("[activate] WDA already alive (tunnel start failed but OK).", flush=True) - return "WDA already alive" - raise + # 2) 启动 tunneld + http_host: Optional[str] = None + http_port: Optional[str] = None # ⚠️ 端口以 str 存储 + rsd_host: Optional[str] = None + rsd_port: Optional[str] = None # ⚠️ 端口以 str 存储 + iface_names: Set[str] = set() - print(f"[tunneld] HTTP tunnel at {http_host}:{http_port}", flush=True) + proc, _port_ignored = self._start_tunneld(udid) + self._live_procs[udid] = proc + self._live_ifaces[udid] = iface_names - # 3) 直接用 HTTP 隧道反复尝试启动 WDA + 探活 - deadline = time.time() + (ready_timeout_sec if ready_timeout_sec > 0 else 60.0) - launched = False + captured: List[str] = [] + out: str = "" + wda_started = False + mount_done = pre_mount_first try: - while time.time() < deadline: - # 已活则成功返回 - if _wda_alive(1.5): - print("[activate] WDA detected alive.", flush=True) - launched = True + assert proc.stdout is not None + for line in proc.stdout: + captured.append(line) + # 日志长度控制,防止常驻时内存涨太多 + if len(captured) > 20000: + captured = captured[-10000:] + + print(f"[tunneld] {line}", end="") + + # 捕获虚拟网卡名 + for m in self.IFACE_RE.finditer(line): + iface_names.add(m.group(1)) + + # 子进程若退出则停止读取 + if proc.poll() is not None: break - # 尝试发起一次 HTTP 启动(失败就下一轮重试) - try: - if hasattr(self, "_launch_wda_via_http_tunnel"): - self._launch_wda_via_http_tunnel( # type: ignore[attr-defined] + # 捕获 HTTP 网关端口(保持为字符串) + if http_port is None: + m = self.HTTP_RE.search(line) + if m: + http_host = m.group(1) + http_port = m.group(2) + # 简单校验 + try: + _ = int(http_port) + except Exception: + print(f"[tunneld] bad http port: {http_port}") + http_host, http_port = None, None + else: + print(f"[tunneld] Tunnel API: {http_host}:{http_port}") + + # 只处理当前 UDID 的 RSD 行 + if not self._line_is_for_udid(line, udid): + continue + + m = self.RSD_CREATED_RE.search(line) or self.RSD_FALLBACK_RE.search(line) + if m and rsd_host is None and rsd_port is None: + rsd_host = m.group(1) + rsd_port = m.group(2) + try: + _ = int(rsd_port) # 仅作数字校验 + except Exception: + print(f"[tunneld] bad rsd port: {rsd_port}") + rsd_host, rsd_port = None, None + else: + print(f"[tunneld] Device-level tunnel ready (RSD {rsd_host}:{rsd_port}).") + + # ========= 尝试启动 WDA ========= + if (not wda_started) and wda_bundle_id and (rsd_host is not None) and (rsd_port is not None): + if not mount_done: + self._auto_mount_developer_disk( + udid, retries=mount_retries, backoff_seconds=backoff_seconds + ) + _t.sleep(2) + mount_done = True + + # RSD 优先;探测时临时转 int;启动命令仍传 str 端口 + rsd_port_int = int(rsd_port) + if self._wait_for_rsd_ready( + rsd_host, rsd_port_int, retries=rsd_probe_retries, delay=rsd_probe_delay_sec + ): + # 这里的实现通常会拼 subprocess 命令行,故端口保持 str + self._launch_wda_via_rsd( bundle_id=wda_bundle_id, - http_host=http_host, - http_port=str(http_port), + rsd_host=rsd_host, + rsd_port=rsd_port, # ⚠️ 传入 str,避免 subprocess 报错 udid=udid, ) - except Exception as e: - # 仅打印,不中断;下一次循环再试 - print(f"[activate] _launch_wda_via_http_tunnel error: {e}", flush=True) + wda_started = True + elif (http_host is not None) and (http_port is not None): + self._launch_wda_via_http_tunnel( + bundle_id=wda_bundle_id, + http_host=http_host, + http_port=http_port, # ⚠️ 传入 str + udid=udid, + ) + wda_started = True + else: + raise RuntimeError("No valid tunnel endpoint for WDA.") - # 启动后给一点时间让 WDA ready - for _ in range(3): - if _wda_alive(1.0): - launched = True + # ✅ WDA 已启动;默认一次性模式直接退出读取循环 + if wda_started and not keep_tunnel: + _t.sleep(0.5) # 给隧道多刷几行 + print("[activate] WDA launched; exiting reader loop (keep_tunnel=False).") break - time.sleep(0.5) - if launched: + # 超时保护(仅在 WDA 尚未启动时生效) + if (not wda_started) and ready_timeout_sec > 0 and (_t.time() - start_ts > ready_timeout_sec): + print(f"[tunneld] Timeout waiting for device tunnel ({ready_timeout_sec}s). Aborting.") break - time.sleep(1.0) # 下一轮重试 - - if not launched: - raise RuntimeError(f"WDA not ready within {ready_timeout_sec}s via HTTP tunnel") - - print("[activate] Done.", flush=True) - return f"http://{http_host}:{http_port}" + print("[activate] 启动 WDA 读取阶段结束") + out = "".join(captured) + except Exception as e: + print(f"[activate] 发生异常:{e}") + raise finally: if not keep_tunnel: try: - self.stop_tunnel(udid, broad_cleanup=broad_cleanup_on_exit) # type: ignore[attr-defined] - except Exception as e: - print(f"[activate] stop_tunnel warn: {e}", flush=True) + self.stop_tunnel(udid, broad_cleanup=broad_cleanup_on_exit) + except Exception as ce: + print(f"[activate] stop_tunnel 清理异常:{ce}") + + print("[activate] Done.") + return out # =============== 外部可显式调用的清理 =============== diff --git a/Module/__pycache__/DeviceInfo.cpython-312.pyc b/Module/__pycache__/DeviceInfo.cpython-312.pyc index b77b316005a84557e2e1a48970f3153562a9df89..b58179f22e0d15580cf4399c99a8f0c387e9ede7 100644 GIT binary patch delta 1132 zcmZXUe@q)?7{}k|u2))W;VAT2#~AEK2iNu}p@bzHC^f}6EecC9vM~zriq#vnLzZBL9no(0micb&tGlJhuZ;kV9u+~LYWo6DTR$6 zk$Qjh-s#Wp9mSYM`rqiy)~tPU`x|&I$~zZ?t=Kg$NV9@8D*qyQhS;^GQO{N0ot7nYeCZwDs;*S#GQLAVc-s;Iqi`+H zdaQ1d)ztFCeZU!Ai$ue6&t|QMiR$O8$d6@Sz2Cqlt4w~5oz?-Rf%E5N;03*)e!BB- zMxW5U$`qd2RRKhu*%e?QPqo)%XP7S2g?bIwtvDO7V!-#3^#^)sh#rarBi%|=nWC2O zFySkKS^TD?0GT=m6sl6)EgkGp`R8`C5K#T~+ZmY0H(neu{HFOuM!AW34G{?8yA4NR z4!Opsv+mGbh$^>e;B9Pbtc9R@qOpg8pD{CV5oYjGzzz!D4cvyLnrsTQrX;mKr1%G) zYPJ_$(PV6cFZ5>kmC&B9j_$HxPo%xCD;SO_-G|>gqQ2A2Gh2SrOud4J{cF<)iXT6x z^AmqgepakZY;K%KiFrm)2%M2`GmHZJr z!fDw9kA_24@U^-#lm&1DpXq22Z_=G7Dbk&BR4r4iQly*zimJ<)=$Phff%C&B>;sY( zi#~Pa)jEJ>{QWhLmD9dyF7{||ZZ(CZ*EmZCW3W!A@ti63v>NEkitfn#}!Y5<3w)nLMh&b7F~ zBjArtGuFrgWZ)TR0Y+pM1pHavnr(@~2>F;h0W6LXAa4?MxC+tiUIkewyGwZM_zJ9Z zix_iBj_m(@0rE+6%=C7m`{CH^7XuI8AM9bZxuM}-CMNEm8-36}@!RAP?UOZrUa$7h4&L`Qa26h) zUw5Qrp3_;qw?sQ`n-=BkqC6J3DXzUG+OFlxNzt`nB-R`){n>18-yR@Ywnqd^Ip2%Y zf#5t()zX4U@``DZMX-@E|Fr12A$pc*+v;6>yczT+*F2$E>|RfIK3QF3C-K##H5KGi zp5G9X_=MeDqT?NyEtZRdtG-f)&?s4-M4D*&|5 zE9*Isw8iSoG>Bt*phvnu+cU< z4y|}=DF@%-rtKrfJJjpo)Y~|>y$jm$<=TC46K85STJEt-h^RlYu)FB2djVRtU3J|Y zOyQlEPQz6^R9^<&I9Y!WE^9}3bn@oQ%=#z8V-&+>a0M&FD`CIZ5YFOsmuMZW4I=Ju zG(rMDXxt`WVOBFkE5lWWIE6!}Zf3fkfqk9y>Ri(rIE=og_aLU7Y5El4S8Qy49p><2 zvs{$Aa5XcsXYSTf*y&PpSt*Xz770GX@|K7AO;f%mw*~>u;u~%2;7jdzo5I2GXjTrw zUpT7xD3i*1_*%=@X#tqQg7#X+ZI*J1A$3AMQ%@L{8B&+{im5mbwNGXFK_7yn{2wGG z<~M0wuhIRU;OP#Zq@#bHdVqO9Vi;$b#^sJv(hQS%3|@u`hC;m1>4I`x=scA9wjQ2) KL>Sc^+WQBpQbxr9 diff --git a/Module/__pycache__/Main.cpython-312.pyc b/Module/__pycache__/Main.cpython-312.pyc index 993b37132f6af8c518b6e69ae2baeaacf281d2d9..1a01700d89464f7fe850dedcfe5510d51b72a7c1 100644 GIT binary patch delta 19 ZcmbOwJ4=@9G%qg~0}z-rZRDE32LLY01a|-c delta 19 ZcmbOwJ4=@9G%qg~0}wR&ZRDE32LLaW1dsp# diff --git a/Utils/__pycache__/LogManager.cpython-312.pyc b/Utils/__pycache__/LogManager.cpython-312.pyc index 5063933152f9f0b88302bb17a462bca5b166d6c1..478fd84ce96fc1b2c926ad5276b7b0bae84a2066 100644 GIT binary patch delta 21 bcmX?Jbi9b?G%qg~0}#~3Fl27zv9<&NOQ;3n delta 21 bcmX?Jbi9b?G%qg~0}yok{mzSYS0|5nL9LN z6pP$=z^xH7s$HzZ#zVR7(j|h*!zw6ZT_VW3>|jOohsr;fzL)3o{qpd956_<&<@`-0 zd(N1eD=U)td*<{avv^^Sc6(QNg%HO@|5gNF1Rhh=7w4UIBis{fKjb`NR<+>pCQ z|EFVctuEGzQ;i~?3+zy}CAG1A*fH6F3js5p4C>(k8a9hKc|*X7;8FbEE21Q8aXr|B zrL>Xc&H=zolZfGr8rMTk?2|-tbvXb+)eD{w!>i$GoUw{H=N0gvIarL;R`8=&SeY6y4Py06pD-iWN>d_7jE>|u4o`00)w8mQ}s6<|AE`dJQl t(!#Ht5?((DL#qJ8`3V8O!G?B~%3YH3<7y~^;j9^sL5Zu5L38D?8`gzON~HsnKx@OhDp=CFa2JNFTkej7feO;BRBaEA@J zH6U|y*iEzz4}uDeEfR)z4WThCW5lP2KjJR#{Slcq;dMtDmW0v^In3{M;Y&z{Grpdh3X#fa#Ob(~ z5NRr3P~D}v!4*Hd z(UR)58r|&E~wuoZG XLU{b1fwE*hsL*Cw&=f~PQQ7|kLq4#I