适配iOS高版本
This commit is contained in:
9
.idea/workspace.xml
generated
9
.idea/workspace.xml
generated
@@ -4,7 +4,14 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成" />
|
||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
||||
<change afterPath="$PROJECT_DIR$/Module/IOSActivator.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Entity/Variables.py" beforeDir="false" afterPath="$PROJECT_DIR$/Entity/Variables.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Module/Main.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/Main.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
|
||||
@@ -3,8 +3,11 @@ from typing import Dict, Any
|
||||
from Entity.AnchorModel import AnchorModel
|
||||
|
||||
# wda apple bundle id
|
||||
WdaAppBundleId = "com.yolozsAgent.wda.xctrunner"
|
||||
|
||||
WdaAppBundleId = "com.yolojtAgent.wda.xctrunner"
|
||||
# wda投屏端口
|
||||
wdaScreenPort = 9567
|
||||
# wda功能端口
|
||||
wdaFunctionPort = 8567
|
||||
# 全局主播列表
|
||||
anchorList: list[AnchorModel] = []
|
||||
# 线程锁
|
||||
|
||||
Binary file not shown.
@@ -1,17 +1,21 @@
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
import tidevice
|
||||
import usb
|
||||
import wda
|
||||
from tidevice import Usbmux, ConnectionType
|
||||
from tidevice._device import BaseDevice
|
||||
from Entity.DeviceModel import DeviceModel
|
||||
from Entity.Variables import WdaAppBundleId
|
||||
from Entity.Variables import WdaAppBundleId, wdaFunctionPort, wdaScreenPort
|
||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||
from Module.IOSActivator import IOSActivator
|
||||
from Utils.LogManager import LogManager
|
||||
|
||||
import socket
|
||||
@@ -36,7 +40,6 @@ class DeviceInfo:
|
||||
orphan_gc_tick = 0
|
||||
while True:
|
||||
online = {d.udid for d in Usbmux().device_list() if d.conn_type == ConnectionType.USB}
|
||||
|
||||
# 拔掉——同步
|
||||
for udid in list(self._models):
|
||||
if udid not in online:
|
||||
@@ -107,7 +110,15 @@ class DeviceInfo:
|
||||
print("进入启动wda方法")
|
||||
try:
|
||||
dev = tidevice.Device(udid)
|
||||
print("获取tidevice对象成功,准备启动wda")
|
||||
systemVersion = int(dev.product_version.split(".")[0])
|
||||
# 判断运行wda的逻辑
|
||||
if systemVersion > 17:
|
||||
ios = IOSActivator()
|
||||
threading.Thread(
|
||||
target=ios.activate,
|
||||
args=(udid,)
|
||||
).start()
|
||||
else:
|
||||
dev.app_start(WdaAppBundleId)
|
||||
print("启动wda成功")
|
||||
time.sleep(3)
|
||||
@@ -118,7 +129,7 @@ class DeviceInfo:
|
||||
|
||||
def _screen_info(self, udid: str):
|
||||
try:
|
||||
c = wda.USBClient(udid, 8100)
|
||||
c = wda.USBClient(udid, wdaFunctionPort)
|
||||
c.home()
|
||||
size = c.window_size()
|
||||
scale = c.scale
|
||||
@@ -139,7 +150,7 @@ class DeviceInfo:
|
||||
flags = subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
|
||||
return subprocess.Popen(
|
||||
[self._iproxy_path, "-u", udid, str(port), "9100"],
|
||||
[self._iproxy_path, "-u", udid, str(port), wdaScreenPort],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=flags
|
||||
|
||||
300
Module/IOSActivator.py
Normal file
300
Module/IOSActivator.py
Normal file
@@ -0,0 +1,300 @@
|
||||
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
from Entity.Variables import WdaAppBundleId
|
||||
|
||||
|
||||
class IOSActivator:
|
||||
"""
|
||||
轻量 iOS 激活器(仅代码调用):
|
||||
1) 启动 `pymobiledevice3 remote tunneld`(基于传入 UDID)
|
||||
2) 自动挂载 Developer Disk Image
|
||||
3) 设备隧道就绪后启动 WDA:
|
||||
- 优先使用 `--rsd <host> <port>` 直连(支持 IPv6)
|
||||
- 失败再用 `PYMOBILEDEVICE3_TUNNEL=127.0.0.1:<port>` 作为退路(仅 IPv4)
|
||||
"""
|
||||
|
||||
def __init__(self, python_executable: Optional[str] = None):
|
||||
self.python = python_executable or sys.executable
|
||||
|
||||
# --------------------------
|
||||
# 内部工具
|
||||
# --------------------------
|
||||
def _auto_mount_developer_disk(self, udid: str, retries: int = 3, backoff_seconds: float = 2.0) -> None:
|
||||
"""使用 `pymobiledevice3 mounter auto-mount` 为指定 UDID 挂载开发者镜像(带重试)。"""
|
||||
env = os.environ.copy()
|
||||
env["PYMOBILEDEVICE3_UDID"] = udid
|
||||
|
||||
max_attempts = max(1, int(retries))
|
||||
last_err = None
|
||||
for i in range(max_attempts):
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
[self.python, "-m", "pymobiledevice3", "mounter", "auto-mount"],
|
||||
text=True,
|
||||
stderr=subprocess.STDOUT,
|
||||
env=env,
|
||||
)
|
||||
if out:
|
||||
for line in out.splitlines():
|
||||
print(f"[mounter] {line}")
|
||||
print("[mounter] Developer disk image mounted.")
|
||||
return
|
||||
except subprocess.CalledProcessError as exc:
|
||||
lowered = (exc.output or "").lower()
|
||||
if "already mounted" in lowered:
|
||||
print("[mounter] Developer disk image already mounted.")
|
||||
return
|
||||
last_err = exc
|
||||
if i < max_attempts - 1:
|
||||
print(f"[mounter] attempt {i+1}/{max_attempts} failed, retrying in {backoff_seconds}s ...")
|
||||
try:
|
||||
import time as _t
|
||||
_t.sleep(backoff_seconds)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
msg = last_err.output if isinstance(last_err, subprocess.CalledProcessError) else str(last_err)
|
||||
raise RuntimeError(f"Auto-mount failed after {max_attempts} attempts: {msg}")
|
||||
|
||||
def _is_ipv4_host(self, host: str) -> bool:
|
||||
return bool(re.match(r"^\d{1,3}(\.\d{1,3}){3}$", host))
|
||||
|
||||
def _wait_for_rsd_ready(self, rsd_host: str, rsd_port: str, retries: int = 5, delay: float = 3.0) -> bool:
|
||||
"""
|
||||
探测 RSD 通道是否就绪:直接尝试 TCP 连接。
|
||||
"""
|
||||
port_int = int(rsd_port)
|
||||
for i in range(1, retries + 1):
|
||||
print(f"[rsd] Probing RSD {rsd_host}:{rsd_port} (attempt {i}/{retries}) ...")
|
||||
try:
|
||||
with socket.create_connection((rsd_host, port_int), timeout=2):
|
||||
print("[rsd] ✅ RSD is reachable and ready.")
|
||||
return True
|
||||
except (socket.timeout, ConnectionRefusedError, OSError) as e:
|
||||
print(f"[rsd] Not ready yet ({e}). Retrying...")
|
||||
import time as _t
|
||||
_t.sleep(delay)
|
||||
print("[rsd] ❌ RSD did not become ready after retries.")
|
||||
return False
|
||||
|
||||
def _launch_wda_via_rsd(self, bundle_id: str, rsd_host: str, rsd_port: str, udid: str) -> None:
|
||||
"""
|
||||
使用 `--rsd <host> <port>` 直连设备隧道来启动 WDA(推荐路径,IPv4/IPv6 都 OK)。
|
||||
不设置 PYMOBILEDEVICE3_TUNNEL,避免 IPv6 解析问题。
|
||||
"""
|
||||
print(f"[wda] Launch via RSD {rsd_host}:{rsd_port}, bundle: {bundle_id}")
|
||||
env = os.environ.copy()
|
||||
env["PYMOBILEDEVICE3_UDID"] = udid
|
||||
|
||||
args = [
|
||||
self.python, "-m", "pymobiledevice3",
|
||||
"developer", "dvt", "launch", bundle_id,
|
||||
"--rsd", rsd_host, rsd_port,
|
||||
]
|
||||
try:
|
||||
out = subprocess.check_output(args, text=True, stderr=subprocess.STDOUT, env=env)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise RuntimeError(f"WDA launch via RSD failed: {exc.output}") from exc
|
||||
|
||||
if out:
|
||||
for line in out.splitlines():
|
||||
print(f"[wda] {line}")
|
||||
print("[wda] Launch via RSD completed.")
|
||||
|
||||
def _launch_wda_via_http_tunnel(self, bundle_id: str, http_host: str, http_port: str, udid: str) -> None:
|
||||
"""
|
||||
退路:通过 HTTP 网关端口设置 PYMOBILEDEVICE3_TUNNEL(仅 IPv4)。
|
||||
"""
|
||||
if not self._is_ipv4_host(http_host):
|
||||
raise RuntimeError(f"HTTP tunnel host must be IPv4, got {http_host}")
|
||||
|
||||
tunnel_endpoint = f"{http_host}:{http_port}"
|
||||
print(f"[wda] Launch via HTTP tunnel {tunnel_endpoint}, bundle: {bundle_id}")
|
||||
env = os.environ.copy()
|
||||
env["PYMOBILEDEVICE3_TUNNEL"] = tunnel_endpoint
|
||||
env["PYMOBILEDEVICE3_UDID"] = udid
|
||||
|
||||
args = [self.python, "-m", "pymobiledevice3", "developer", "dvt", "launch", bundle_id]
|
||||
try:
|
||||
out = subprocess.check_output(args, text=True, stderr=subprocess.STDOUT, env=env)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise RuntimeError(f"WDA launch via HTTP tunnel failed: {exc.output}") from exc
|
||||
|
||||
if out:
|
||||
for line in out.splitlines():
|
||||
print(f"[wda] {line}")
|
||||
print("[wda] Launch via HTTP tunnel completed.")
|
||||
|
||||
def _pick_available_port(self, base=49151, step=10) -> int:
|
||||
"""挑选一个可用端口(避免重复)"""
|
||||
for i in range(0, 1000, step):
|
||||
port = base + i
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
if s.connect_ex(("127.0.0.1", port)) != 0:
|
||||
return port
|
||||
raise RuntimeError("No free port found for tunneld")
|
||||
|
||||
# --------------------------
|
||||
# 对外方法
|
||||
# --------------------------
|
||||
def activate(
|
||||
self,
|
||||
udid: str,
|
||||
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,
|
||||
) -> str:
|
||||
"""
|
||||
执行:开隧道 -> (等待 RSD 就绪)-> 挂载镜像 -> 启动 WDA
|
||||
- 优先用 `--rsd` 启动(先做 dvt list 探测)
|
||||
- 失败再用 HTTP 端口作为退路
|
||||
"""
|
||||
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}")
|
||||
env = os.environ.copy()
|
||||
env["PYMOBILEDEVICE3_UDID"] = udid
|
||||
|
||||
# 1) 开隧道(子进程常驻)
|
||||
port = self._pick_available_port()
|
||||
cmd = [self.python, "-m", "pymobiledevice3", "remote", "tunneld", "--port", str(port)]
|
||||
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
captured: list[str] = []
|
||||
http_host: Optional[str] = None
|
||||
http_port: Optional[str] = None
|
||||
rsd_host: Optional[str] = None
|
||||
rsd_port: Optional[str] = None
|
||||
device_tunnel_ready = False
|
||||
wda_started = False
|
||||
mount_done = False
|
||||
|
||||
import time as _t
|
||||
start_ts = _t.time()
|
||||
|
||||
HTTP_RE = re.compile(r"http://([0-9.]+):(\d+)")
|
||||
RSD_CREATED_RE = re.compile(r"Created tunnel\s+--rsd\s+([^\s]+)\s+(\d+)")
|
||||
|
||||
try:
|
||||
assert proc.stdout is not None
|
||||
for line in proc.stdout:
|
||||
captured.append(line)
|
||||
print(f"[tunneld] {line}", end="")
|
||||
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
|
||||
# 捕获 HTTP 网关端口
|
||||
if http_port is None:
|
||||
m = HTTP_RE.search(line)
|
||||
if m:
|
||||
http_host, http_port = m.group(1), m.group(2)
|
||||
print(f"[tunneld] Tunnel API: {http_host}:{http_port}")
|
||||
|
||||
# 捕获设备 RSD(可能 IPv6)
|
||||
m = RSD_CREATED_RE.search(line)
|
||||
if m:
|
||||
rsd_host, rsd_port = m.group(1), m.group(2)
|
||||
device_tunnel_ready = True
|
||||
print(f"[tunneld] Device-level tunnel ready (RSD {rsd_host}:{rsd_port}).")
|
||||
|
||||
# 条件满足后推进
|
||||
if (not wda_started) and wda_bundle_id and device_tunnel_ready:
|
||||
try:
|
||||
if not mount_done:
|
||||
self._auto_mount_developer_disk(
|
||||
udid, retries=mount_retries, backoff_seconds=backoff_seconds
|
||||
)
|
||||
mount_done = True
|
||||
|
||||
# 先做 RSD 就绪探测,再走 RSD 启动
|
||||
rsd_ok = False
|
||||
if rsd_host and rsd_port:
|
||||
rsd_ok = self._wait_for_rsd_ready(
|
||||
rsd_host, rsd_port,
|
||||
retries=rsd_probe_retries,
|
||||
delay=rsd_probe_delay_sec,
|
||||
)
|
||||
if rsd_ok:
|
||||
self._launch_wda_via_rsd(
|
||||
bundle_id=wda_bundle_id,
|
||||
rsd_host=rsd_host, # type: ignore[arg-type]
|
||||
rsd_port=rsd_port, # type: ignore[arg-type]
|
||||
udid=udid,
|
||||
)
|
||||
else:
|
||||
# RSD 不就绪或失败,回退到 HTTP 网关(必须是 IPv4)
|
||||
if http_host and http_port:
|
||||
self._launch_wda_via_http_tunnel(
|
||||
bundle_id=wda_bundle_id,
|
||||
http_host=http_host,
|
||||
http_port=http_port,
|
||||
udid=udid,
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("No valid tunnel endpoint for fallback.")
|
||||
|
||||
wda_started = True
|
||||
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
raise
|
||||
|
||||
# 超时保护(仍未启动 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.")
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
break
|
||||
|
||||
# 等待子进程结束(若已结束)
|
||||
try:
|
||||
return_code = proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return_code = proc.returncode or -9
|
||||
|
||||
output = "".join(captured)
|
||||
if return_code != 0 and not wda_started:
|
||||
raise RuntimeError(f"tunneld exited with code {return_code}.\n{output}")
|
||||
|
||||
print("[activate] Done.")
|
||||
return output
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n[activate] Interrupted, cleaning up ...", file=sys.stderr)
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
@@ -36,7 +36,7 @@ def main(arg):
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 获取启动时候传递的参数
|
||||
main(sys.argv)
|
||||
# main(sys.argv)
|
||||
|
||||
# 添加iOS开发包到电脑上
|
||||
deployer = DevDiskImageDeployer(verbose=True)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -26,7 +26,7 @@ def _force_utf8_everywhere():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_force_utf8_everywhere()
|
||||
# _force_utf8_everywhere()
|
||||
|
||||
class LogManager:
|
||||
"""
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user