Files
jbjf6666 85e9a53750 docs: 新增 Windows 移植交接文档
docs/WINDOWS_PORT.md 覆盖:
- 逐文件可移植性审计(核心逻辑层约 90% 可直接复用)
- 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC +
  ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道
- 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter
  auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel)
- 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico)
- macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils
  放进 excludes、AlreadyMountedError 当成功处理
- 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等)
- 验收基线(复用现有测试套件)与建议的工作顺序
2026-10-16 01:05:09 +08:00

225 lines
8.6 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""列出 iOS 设备并打印 iOS 版本 / DVT 虚拟定位可用性。
用法(项目根目录下执行):
./.venv/bin/python tools/probe_device.py
./.venv/bin/python tools/probe_device.py --rsd 10.7.0.1 51234
./.venv/bin/python tools/probe_device.py --json
说明:
* 没有连接设备时打印中文提示并以退出码 2 结束,不会抛栈。
* ``--rsd HOST PORT`` 时会对 iOS 17+ 设备真正建立一次 RSD+DVT 会话做连通性验证
(不设置任何位置,验证后立即断开)。
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.location_provider import ( # noqa: E402
PMD3_MISSING_HINT,
RealLocationProvider,
pymobiledevice3_version,
pymobiledevice3_available,
)
#: iOS 17 起 DVT 服务只能通过 RSD 隧道访问
RSD_REQUIRED_IOS_MAJOR = 17
def _print(text: str = "") -> None:
print(text, flush=True)
async def _enumerate_devices(udid: str | None) -> list[dict]:
"""优先用 lead 的 core/device.py,不可用时退回直接查 usbmux。"""
devices: list[dict] = []
try: # lead 的 DeviceManager(可能尚未实现或签名不同,失败则退化)
from core.device import DeviceManager # type: ignore
for factory in (lambda: DeviceManager(), lambda: DeviceManager(mock=False)):
try:
manager = factory()
raw = manager.list_devices()
if asyncio.iscoroutine(raw):
raw = await raw
for item in raw or []:
devices.append(dict(item))
if devices:
return devices
except Exception:
continue
except Exception:
pass
# 退化路径:直接用 pymobiledevice3 的 usbmux
from pymobiledevice3 import usbmux
from pymobiledevice3.lockdown import create_using_usbmux
for mux_device in await usbmux.list_devices():
if udid and not mux_device.matches_udid(udid):
continue
entry: dict = {
"udid": mux_device.serial,
"connection": (mux_device.connection_type or "").lower(),
"name": None,
"product_type": None,
"ios_version": None,
"ios_major": 0,
"developer_mode": None,
"state": "connected",
"error": None,
}
try:
lockdown = await create_using_usbmux(serial=mux_device.serial, autopair=False)
except Exception as exc:
entry["state"] = "unpaired"
entry["error"] = f"{type(exc).__name__}: {exc}"
devices.append(entry)
continue
try:
entry["name"] = lockdown.all_values.get("DeviceName")
entry["product_type"] = lockdown.all_values.get("ProductType")
version = str(lockdown.product_version or "")
entry["ios_version"] = version
try:
entry["ios_major"] = int(version.split(".", 1)[0])
except (ValueError, IndexError):
entry["ios_major"] = 0
try:
entry["developer_mode"] = await lockdown.get_developer_mode_status()
except Exception:
entry["developer_mode"] = None
finally:
try:
await lockdown.close()
except Exception:
pass
devices.append(entry)
return devices
def _judge(device: dict, rsd: tuple | None) -> tuple[bool, str]:
"""返回 (DVT 是否可能可用, 中文说明)。"""
major = int(device.get("ios_major") or 0)
state = device.get("state")
if state == "unpaired":
return False, "未配对:请在 iPhone 上点击「信任此电脑」后重试"
if device.get("developer_mode") is False:
return False, "未开启开发者模式:设置 → 隐私与安全性 → 开发者模式(iOS16+ 必须开启)"
if major >= RSD_REQUIRED_IOS_MAJOR:
if rsd:
return True, f"iOS {major} ≥ 17:需要 RSD 隧道,已提供 {rsd[0]}:{rsd[1]}(待实测)"
return False, (
f"iOS {major} ≥ 17:必须提供 RSD 隧道(--rsd HOST PORT)。"
"免 root:pymobiledevice3 remote start-tunnel --native --script-mode;"
"需 root:sudo pymobiledevice3 remote start-tunnel --script-mode"
)
if major == 0:
return False, "无法识别 iOS 版本:设备可能未解锁或未配对"
return True, f"iOS {major} < 17:可经 usbmux/lockdown 直连 DVT(或老版 simulatelocation 服务)"
async def _probe_dvt(device: dict, rsd: tuple | None) -> dict:
"""真正建立一次会话验证 DVT 是否可用(不设置位置)。"""
provider = RealLocationProvider(
rsd_host=rsd[0] if rsd else None,
rsd_port=rsd[1] if rsd else None,
udid=device.get("udid"),
)
try:
info = await provider.connect(device)
except Exception as exc:
return {"ok": False, "detail": str(exc)}
try:
await provider.close()
except Exception:
pass
return {"ok": True, "detail": info.get("detail", "连接成功"), "strategy": info.get("strategy")}
async def _main_async(args: argparse.Namespace) -> int:
rsd = (args.rsd[0], int(args.rsd[1])) if args.rsd else None
if not pymobiledevice3_available():
_print(f"[错误] {PMD3_MISSING_HINT}")
return 1
_print(f"pymobiledevice3 版本:{pymobiledevice3_version()}")
_print(f"Python:{sys.version.split()[0]}")
if rsd:
_print(f"RSD 隧道:{rsd[0]}:{rsd[1]}")
_print("")
try:
devices = await _enumerate_devices(args.udid)
except Exception as exc:
_print(f"[错误] 枚举设备失败:{type(exc).__name__}: {exc}")
return 1
if not devices:
_print("未发现 iOS 设备。请:")
_print(" 1) 用 USB 数据线连接 iPhone(不是仅充电线);")
_print(" 2) 解锁手机并在弹窗中点击「信任此电脑」;")
_print(" 3) 若使用 Wi-Fi 连接,请确认与电脑在同一网络。")
return 2
results = []
for device in devices:
available, reason = _judge(device, rsd)
entry = {"device": device, "available": available, "reason": reason, "probe": None}
if args.probe and available:
entry["probe"] = await _probe_dvt(device, rsd)
entry["available"] = bool(entry["probe"].get("ok"))
results.append(entry)
if args.json:
_print(json.dumps(results, ensure_ascii=False, indent=2))
return 0
for index, entry in enumerate(results, 1):
device = entry["device"]
_print(f"=== 设备 {index} ===")
_print(f" UDID : {device.get('udid')}")
_print(f" 名称 : {device.get('name') or '(未知)'}")
_print(f" 型号 : {device.get('product_type') or '(未知)'}")
_print(f" iOS 版本 : {device.get('ios_version') or '(未知)'} (major={device.get('ios_major')})")
_print(f" 连接方式 : {device.get('connection') or '(未知)'}")
_print(f" 配对状态 : {device.get('state')}")
developer_mode = device.get("developer_mode")
_print(f" 开发者模式 : {'已开启' if developer_mode else ('未开启' if developer_mode is False else '未知')}")
if device.get("error"):
_print(f" 错误 : {device['error']}")
_print(f" DVT 可用性 : {'可能可用' if entry['available'] else '不可用'} —— {entry['reason']}")
if entry["probe"]:
probe = entry["probe"]
_print(f" 实测连接 : {'成功' if probe.get('ok') else '失败'} —— {probe.get('detail')}")
_print("")
_print("提示:iOS 17+ 必须先建立 RSD 隧道;不带 --rsd 时只做静态判断。")
_print(" 加 --probe --rsd HOST PORT 可实测建立一次 DVT 会话。")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="列出 iOS 设备并检查 DVT 虚拟定位可用性")
parser.add_argument("--rsd", nargs=2, metavar=("HOST", "PORT"), help="RSD 隧道地址(iOS 17+ 必需)")
parser.add_argument("--udid", help="只检查指定 UDID")
parser.add_argument("--probe", action="store_true", help="真正建立一次 DVT 会话做连通性验证")
parser.add_argument("--json", action="store_true", help="以 JSON 输出")
args = parser.parse_args()
try:
return asyncio.run(_main_async(args))
except KeyboardInterrupt:
return 130
if __name__ == "__main__":
sys.exit(main())