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 分离等) - 验收基线(复用现有测试套件)与建议的工作顺序
143 lines
5.4 KiB
Python
143 lines
5.4 KiB
Python
#!/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())
|