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

+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""命令行测试真机虚拟定位(单点设置 / 清除)。
用法(项目根目录下执行):
# iOS<17:USB 直连,设置单点位置并保持(Ctrl-C 结束,结束时自动恢复真实定位)
./.venv/bin/python tools/probe_location.py --lat 39.908 --lon 116.397
# iOS 17+:先建隧道再指定 RSD 地址
./.venv/bin/python tools/probe_location.py --rsd 10.7.0.1 51234 --lat 39.908 --lon 116.397
# 只恢复真实定位
./.venv/bin/python tools/probe_location.py --rsd 10.7.0.1 51234 --clear
# 自动化:只保持 10 秒后自动清除
./.venv/bin/python tools/probe_location.py --lat 39.908 --lon 116.397 --duration 10
注意:DVT 虚拟定位依赖**保持连接**(连接断开即失效),所以本工具在 --duration 期间
一直持有同一个 DVT 会话;退出前会调用 clear_location() 让手机恢复真实定位。
"""
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_available,
pymobiledevice3_version,
)
def _print(text: str = "") -> None:
print(text, flush=True)
def _log(level: str, msg: str) -> None:
prefix = {"error": "[错误]", "warning": "[警告]", "success": "[成功]"}.get(level, "[信息]")
_print(f"{prefix} {msg}")
async def _main_async(args: argparse.Namespace) -> int:
if not pymobiledevice3_available():
_print(f"[错误] {PMD3_MISSING_HINT}")
return 1
if not args.clear and (args.lat is None or args.lon is None):
_print("[错误] 需要同时提供 --lat 与 --lon(或使用 --clear 仅恢复真实定位)")
return 1
provider = RealLocationProvider(
rsd_host=args.rsd[0] if args.rsd else None,
rsd_port=int(args.rsd[1]) if args.rsd else None,
udid=args.udid,
connect_timeout=args.timeout,
on_log=_log,
)
_print(f"pymobiledevice3 版本:{pymobiledevice3_version()}")
if args.rsd:
_print(f"RSD 隧道:{args.rsd[0]}:{args.rsd[1]}")
else:
_print("未指定 --rsd:将尝试 usbmux/lockdown 直连(仅 iOS<17 可用)")
_print("")
try:
info = await provider.connect(None)
except Exception as exc:
_print(f"[错误] 连接失败:{exc}")
return 1
_print(f"[信息] 会话已建立:{info.get('detail')}")
_print(f"[信息] 策略={info.get('strategy')} iOS={info.get('ios_version') or info.get('ios_major')}")
exit_code = 0
try:
if args.clear:
await provider.clear_location()
_print("[成功] 已恢复真实定位")
else:
await provider.set_location(float(args.lat), float(args.lon))
_print(f"[成功] 已设置虚拟定位 lat={float(args.lat):.6f}, lon={float(args.lon):.6f}")
if args.json:
_print(json.dumps({"ok": True, "lat": args.lat, "lon": args.lon}, ensure_ascii=False))
if args.duration and args.duration > 0:
_print(f"[信息] 保持 {args.duration:.0f} 秒(期间不要断开连接)…")
await asyncio.sleep(args.duration)
else:
_print("[信息] 定位保持中:按 Ctrl-C 结束并恢复真实定位…")
try:
while True:
await asyncio.sleep(1.0)
except (KeyboardInterrupt, asyncio.CancelledError):
_print("")
_print("[信息] 收到中断,准备恢复真实定位…")
except KeyboardInterrupt:
_print("")
_print("[信息] 收到中断,准备恢复真实定位…")
except Exception as exc:
_print(f"[错误] {exc}")
exit_code = 1
finally:
try:
await provider.clear_location()
_print("[信息] 已调用 clear_location(),设备恢复真实定位")
except Exception as exc:
_print(f"[警告] 恢复真实定位失败:{exc}")
exit_code = exit_code or 1
try:
await provider.close()
except Exception as exc:
_print(f"[警告] 关闭会话时出错:{exc}")
return exit_code
def main() -> int:
parser = argparse.ArgumentParser(description="命令行测试真机虚拟定位(DVT)")
parser.add_argument("--lat", type=float, help="纬度(-90~90,WGS84)")
parser.add_argument("--lon", type=float, help="经度(-180~180,WGS84)")
parser.add_argument("--clear", action="store_true", help="只清除虚拟定位,恢复真实定位")
parser.add_argument("--rsd", nargs=2, metavar=("HOST", "PORT"), help="RSD 隧道地址(iOS 17+ 必需)")
parser.add_argument("--udid", help="目标设备 UDID(默认第一台 USB 设备)")
parser.add_argument("--duration", type=float, default=0.0, help="保持秒数;0 表示等待 Ctrl-C(默认 0)")
parser.add_argument("--timeout", type=float, default=15.0, help="建立会话超时秒数(默认 15)")
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())