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 分离等)
- 验收基线(复用现有测试套件)与建议的工作顺序
This commit is contained in:
jbjf6666 committed 2026-10-16 01:05:09 +08:00
commit 85e9a53750
65 files changed
+18603

No files matched your search

+224
View File
@@ -0,0 +1,224 @@
#!/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())