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

143 lines
5.4 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<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())