feat(windows): 新增 Windows 移植(免管理员隧道 + 自动装 Apple 驱动 + 环境自检)
按 docs/WINDOWS_PORT.md 完成 Windows 移植,并修正该文档中若干与实测不符之处。
macOS 构建链完整保留,单一代码库双平台构建。
核心:iOS 17+ RSD 隧道在 Windows 上换了实现方式
- macOS 借 Apple remoted(--native,内核可路由),Windows 改走 UserspaceRsdTunnel
(纯 Python PyTCP 用户态协议栈)
- 实测:不需要管理员权限、不需要 Wintun 驱动(pytun_pmd3 自带 wintun.dll)、
建立耗时约 0.3 秒
- 关键坑:用户态隧道的设备地址**只在本进程可达**。拿 (host, port) 另建 RSD 会得到
OSError [WinError 1231] 网络位置不可达。必须把 aopen() 返回的 rsd 对象本身注入
DVT 层(RealLocationProvider 早有 rsd= 注入口),因此 Windows 不能沿用
「隧道助手子进程 + 解析 HOST PORT」那套架构
- 修正 WINDOWS_PORT.md 的错误:类名是 UserspaceRsdTunnel 不是 UserspaceTunnel;
remote start-tunnel 没有 --userspace;Wintun 不是必需项
新增
- core/platform.py 平台抽象层(数据目录/浏览器探测/进程管理/提权/信号)
- core/apple_driver.py Apple Mobile Device Support 检测与自动安装
(服务启动 → 独立 AMDS MSI → 完整 iTunes 包 → winget 逐级降级)
+ 开发者镜像下载
- core/setup_window.py 启动时的「环境准备」窗口:逐项列出依赖,缺失项右侧可点安装,
必需项全部就绪后「进入程序」才可点击
- scripts/run.ps1, build.ps1, installer/iosloc.iss, installer/version_info.txt
- tools/make_ico.py(由已有 PNG 图标集生成多尺寸 .ico)
- tools/probe_tunnel_win.py(隧道可行性诊断)
- tests/win_e2e.py(7 项)、tests/win_app_e2e.py(9 项)真机验收
- docs/README-WINDOWS.md
- .gitattributes(避免 Windows 写出 CRLF 的 .sh 把 macOS 构建搞坏)
修改
- core/tunnel.py Windows 走进程内 userspace 隧道;macOS 路径不变
- core/server.py TunnelManager 注入 runner;provider_for 注入 rsd 对象;
资源目录与数据目录分离;日志落盘
- core/location_provider.py 平台化错误提示;新增保活(隧道静默断开会让设备
偷偷恢复真实定位,停止播放时每 15s 重发)
- main.py 「环境准备」窗口、Apple 驱动后台检查、
--check-driver / --install-driver、跨平台浏览器与信号
- Location.spec 平台分支;Windows 用 .ico + uac_admin=True(启动即请求管理员)
+ version_info.txt;单一 spec 不分叉
- tests/drift_sim.py / offline_tiles.py 去掉硬编码 /tmp,改用 tempfile.gettempdir()
打包
- exe 清单写入 requireAdministrator(用户要求:启动时直接请求管理员权限)
- 绿色版 dist\iOSLocationTrack\iOSLocationTrack.exe
- 安装包 dist\installer\iOSLocationTrack-1.0.0-setup.exe(Inno Setup)
实测(Windows 11 x64 / Python 3.12.14 / pymobiledevice3 11.31.0 / iPhone 11 iOS 26.6.1)
- 隧道→镜像→DVT→下发定位→恢复真实定位 全链路跑通,连接 0.8s
- selftest 83/83、drift_sim 40/40、offline_tiles 13/13
- win_e2e 7/7、win_app_e2e 9/9
- 打包版 exe:发现设备 → 0.3s 隧道 → DVT → 两次下发 → 恢复真实定位 → 干净退出
This commit is contained in:
1 parent
85e9a53750
commit
4d55e2415f
26 files changed
+4515
-306
No files matched your search
+41
-4
@@ -85,8 +85,12 @@ class AsyncRunner:
|
||||
class App:
|
||||
"""应用装配:配置、设备、隧道、定位会话、路由、播放、瓦片、日志。"""
|
||||
|
||||
def __init__(self, root: Path, mock: bool = False, port: int = protocol.DEFAULT_PORT) -> None:
|
||||
def __init__(self, root: Path, mock: bool = False, port: int = protocol.DEFAULT_PORT,
|
||||
resources: Path | str | None = None) -> None:
|
||||
# root = 可写数据目录(打包后是 %LOCALAPPDATA%\\iOSLocationTrack)
|
||||
# resources = 只读资源目录(打包后是 PyInstaller 解包目录),默认与 root 相同
|
||||
self.root = Path(root)
|
||||
self.resources = Path(resources) if resources is not None else self.root
|
||||
self.mock = mock
|
||||
self.port = port
|
||||
self.host = protocol.DEFAULT_HOST
|
||||
@@ -100,13 +104,18 @@ class App:
|
||||
pass
|
||||
self.runner = AsyncRunner()
|
||||
self.hub = ws.Hub()
|
||||
self.tunnel = tunnel_mod.TunnelManager(log=self.log, on_change=self._on_tunnel_change)
|
||||
# Windows 的 userspace 隧道必须建在应用自己的事件循环上(地址仅本进程可达),
|
||||
# 因此把 runner 注入 TunnelManager;macOS 只用它做子进程管理,传了也无害。
|
||||
self.tunnel = tunnel_mod.TunnelManager(
|
||||
log=self.log, on_change=self._on_tunnel_change, runner=self.runner
|
||||
)
|
||||
self.devices = device_mod.DeviceManager(mock=mock, on_change=self._on_devices_change)
|
||||
self.routes = route_engine.RouteEngine(self.config)
|
||||
self.tiles = tilecache.TileCache(
|
||||
self.config.cache_dir(), int(self.config.get("tile_cache_max_mb") or 1024)
|
||||
)
|
||||
self.logs: deque[dict[str, Any]] = deque(maxlen=500)
|
||||
self._log_file = self._open_log_file()
|
||||
self._lock = threading.RLock()
|
||||
self.provider: Any = None
|
||||
self.provider_udid: str | None = None
|
||||
@@ -126,10 +135,27 @@ class App:
|
||||
self._mount_running = False
|
||||
|
||||
# ---------------- 日志 ----------------
|
||||
def _open_log_file(self):
|
||||
"""打包成窗口程序后没有控制台,日志必须落盘,否则出问题无从排查。"""
|
||||
try:
|
||||
directory = self.root / "logs"
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
handle = open(directory / "app.log", "a", encoding="utf-8")
|
||||
return handle
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def log(self, level: str, message: str) -> None:
|
||||
entry = {"ts": protocol.now_ts(), "level": level, "msg": str(message)}
|
||||
self.logs.append(entry)
|
||||
self.hub.broadcast({"type": "log", **entry})
|
||||
handle = getattr(self, "_log_file", None)
|
||||
if handle is not None:
|
||||
try:
|
||||
handle.write(f"{entry['ts']}\t{level}\t{message}\n")
|
||||
handle.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def recent_logs(self, limit: int = 200) -> list[dict[str, Any]]:
|
||||
try:
|
||||
@@ -218,7 +244,13 @@ class App:
|
||||
# 而且如果同时插着另一台可用的设备,要直接告诉用户换哪台。
|
||||
self._preflight(device)
|
||||
|
||||
# RSD 有两种形态,必须区分(这是 Windows 与 macOS 最大的架构差异):
|
||||
# - macOS native 隧道:地址内核可路由 → 传 (host, port) 让 provider 自己连
|
||||
# - Windows userspace 隧道:地址只在**本进程的用户态协议栈**里可达,
|
||||
# 直接 TCP 连接会得到 OSError [WinError 1231] 网络位置不可达 →
|
||||
# 必须把隧道返回的那个已连接 RSD 对象本身注入给 provider
|
||||
rsd = None
|
||||
rsd_obj = None
|
||||
if (device.ios_major or 0) >= 17:
|
||||
if not self.tunnel.ready:
|
||||
snapshot = self.tunnel.ensure(device.udid, allow_sudo=False)
|
||||
@@ -228,8 +260,11 @@ class App:
|
||||
"tunnel_required",
|
||||
status=409,
|
||||
)
|
||||
rsd = self.tunnel.address()
|
||||
rsd_obj = self.tunnel.rsd_object()
|
||||
if rsd_obj is None:
|
||||
rsd = self.tunnel.address()
|
||||
provider = RealLocationProvider(
|
||||
rsd=rsd_obj,
|
||||
rsd_host=rsd[0] if rsd else None,
|
||||
rsd_port=rsd[1] if rsd else None,
|
||||
udid=device.udid,
|
||||
@@ -264,7 +299,7 @@ class App:
|
||||
hint = f";建议改用另一台设备:{best.name}(iOS {best.ios_version},{best.connection.upper()})"
|
||||
raise protocol.AppError(
|
||||
f"{device.name}(iOS {device.ios_version})尚未挂载开发者镜像,无法做虚拟定位。"
|
||||
f"请执行:./.venv/bin/python -m pymobiledevice3 mounter auto-mount --udid {device.udid}{hint}",
|
||||
f"请执行:{tunnel_mod.mounter_hint(device.udid)}{hint}",
|
||||
"developer_image_missing",
|
||||
status=409,
|
||||
)
|
||||
@@ -779,6 +814,8 @@ class App:
|
||||
base = getattr(sys, "_MEIPASS", None)
|
||||
if base:
|
||||
sources.append(Path(base) / "resources" / "ddi" / "personalized")
|
||||
# 打包后 root 是可写数据目录,素材在 resources(只读包内目录)
|
||||
sources.append(self.resources / "resources" / "ddi" / "personalized")
|
||||
sources.append(self.root / "resources" / "ddi" / "personalized")
|
||||
sources.append(Path.home() / ".ios-location-ddi" / "personalized")
|
||||
|
||||
|
||||
Reference in new issue
Block a user