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:
jbjf6666 committed 2026-10-16 01:41:59 +08:00
1 parent 85e9a53750
commit 4d55e2415f
26 files changed
+4515 -306

No files matched your search

+162
View File
@@ -0,0 +1,162 @@
# iOS 虚拟定位轨迹工具 —— Windows 构建脚本
#
# 产出:
# dist\iOSLocationTrack\iOSLocationTrack.exe 绿色版(直接双击运行)
# dist\installer\iOSLocationTrack-1.0.0-setup.exe 安装包(需要 Inno Setup 6)
#
# 用法(在项目根目录):
# .\scripts\build.ps1 # 打包 exe + 安装包(若装了 Inno Setup)
# .\scripts\build.ps1 -NoInstaller # 只打包 exe
# .\scripts\build.ps1 -SkipTests # 跳过测试
#
# 前置:
# 1. 已运行过 .\scripts\run.ps1(或手动建好 .venv 并装好依赖)
# 2. 图标:resources\AppIcon.ico(缺失时自动用 tools\make_ico.py 生成)
# 3. 开发者镜像:resources\ddi\personalized\Image.dmg(缺失时自动下载)
# 4. Inno Setup 6(可选):winget install -e --id JRSoftware.InnoSetup
[CmdletBinding()]
param(
[switch]$NoInstaller,
[switch]$SkipTests,
[switch]$SkipIcon,
[switch]$SkipDdi
)
$ErrorActionPreference = 'Stop'
function Write-Bold { param($m) Write-Host "`n=== $m ===" -ForegroundColor White }
function Write-Ok { param($m) Write-Host " [OK] $m" -ForegroundColor Green }
function Write-Warn { param($m) Write-Host " [!!] $m" -ForegroundColor Yellow }
function Write-Fail { param($m) Write-Host " [XX] $m" -ForegroundColor Red }
$Root = Split-Path -Parent $PSScriptRoot
Set-Location $Root
$VenvPy = Join-Path $Root '.venv\Scripts\python.exe'
if (-not (Test-Path $VenvPy)) {
Write-Fail "找不到 $VenvPy。请先运行 .\scripts\run.ps1 创建环境。"
exit 1
}
# ---------- 0. 构建依赖 ----------
Write-Bold "0. 构建依赖"
& $VenvPy -c "import PyInstaller, PIL" 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Warn "缺少 PyInstaller / Pillow,正在安装…"
$idx = if ($env:IOSLOC_PIP_INDEX) { $env:IOSLOC_PIP_INDEX } else { 'https://mirrors.aliyun.com/pypi/simple/' }
& $VenvPy -m pip install --disable-pip-version-check -i $idx pyinstaller pillow
if ($LASTEXITCODE -ne 0) { Write-Fail "构建依赖安装失败"; exit 1 }
}
Write-Ok "PyInstaller / Pillow 就绪"
# ---------- 1. 图标 ----------
if (-not $SkipIcon) {
Write-Bold "1. Windows 图标(.ico)"
if ((Test-Path 'resources\AppIcon.ico') -and
((Get-Item 'resources\AppIcon.ico').LastWriteTime -gt (Get-Item 'resources\AppIcon.iconset').LastWriteTime)) {
Write-Ok "resources\AppIcon.ico 已是最新"
} else {
& $VenvPy tools\make_ico.py
if ($LASTEXITCODE -ne 0) { Write-Fail "图标生成失败"; exit 1 }
}
}
# ---------- 2. 开发者镜像 ----------
if (-not $SkipDdi) {
Write-Bold "2. 开发者镜像素材"
$imageDmg = 'resources\ddi\personalized\Image.dmg'
if (Test-Path $imageDmg) {
$mb = [math]::Round((Get-Item $imageDmg).Length / 1MB, 2)
Write-Ok "$imageDmg 已存在($mb MB)"
} else {
Write-Warn "$imageDmg 缺失(仓库中不入库),正在下载…"
& $VenvPy tools\fetch_ddi.py --personalized-only
if (-not (Test-Path $imageDmg)) {
Write-Fail "镜像下载失败。iOS 17+ 挂载会退化为在线获取,打包产物不完整。"
Write-Fail "可稍后重试:$VenvPy tools\fetch_ddi.py --personalized-only"
exit 1
}
}
}
# ---------- 3. 测试 ----------
if (-not $SkipTests) {
Write-Bold "3. 测试(无需设备)"
$failed = 0
foreach ($t in @('tests\selftest.py', 'tests\drift_sim.py', 'tests\offline_tiles.py')) {
Write-Host " --- $t"
& $VenvPy $t 2>&1 | Select-Object -Last 3
if ($LASTEXITCODE -ne 0) { $failed++; Write-Fail "$t 失败" }
}
if ($failed -gt 0) { Write-Fail "$failed 个测试套件失败,已中止打包"; exit 1 }
Write-Ok "全部测试通过"
}
# ---------- 4. PyInstaller ----------
Write-Bold "4. 打包 exe(onedir + requireAdministrator 清单)"
if (Test-Path 'dist\iOSLocationTrack') { Remove-Item 'dist\iOSLocationTrack' -Recurse -Force }
& $VenvPy -m PyInstaller --clean --noconfirm Location.spec
if ($LASTEXITCODE -ne 0) { Write-Fail "PyInstaller 打包失败"; exit 1 }
$exe = 'dist\iOSLocationTrack\iOSLocationTrack.exe'
if (-not (Test-Path $exe)) { Write-Fail "未生成 $exe"; exit 1 }
$sizeMb = [math]::Round(((Get-ChildItem 'dist\iOSLocationTrack' -Recurse -File |
Measure-Object Length -Sum).Sum / 1MB), 1)
Write-Ok "$exe(整包 $sizeMb MB)"
# ---------- 5. 校验清单里的 requireAdministrator ----------
Write-Bold "5. 校验 exe 是否请求管理员权限"
$manifestOk = $false
try {
# manifest 嵌在 exe 资源里;用 findstr 直接找 UTF-8 明文即可
$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $exe))
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text -match 'requireAdministrator') {
$manifestOk = $true
Write-Ok "清单包含 requireAdministrator(启动时会弹 UAC)"
}
} catch { }
if (-not $manifestOk) {
Write-Warn "未能在 exe 中确认 requireAdministrator —— 请手动确认 Location.spec 的 uac_admin=True"
}
# ---------- 6. Inno Setup ----------
if ($NoInstaller) {
Write-Bold "6. 跳过安装包(-NoInstaller)"
} else {
Write-Bold "6. 制作安装包(Inno Setup)"
$iscc = $null
foreach ($candidate in @(
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe",
"$env:LOCALAPPDATA\Inno Setup 6\ISCC.exe"
)) {
if ($candidate -and (Test-Path $candidate)) { $iscc = $candidate; break }
}
if (-not $iscc) {
$cmd = Get-Command ISCC.exe -ErrorAction SilentlyContinue
if ($cmd) { $iscc = $cmd.Source }
}
if (-not $iscc) {
Write-Warn "未找到 Inno Setup 6,跳过安装包。"
Write-Warn "安装:winget install -e --id JRSoftware.InnoSetup"
Write-Warn "然后重跑:.\scripts\build.ps1 -SkipTests -SkipIcon -SkipDdi"
} else {
Write-Host " 使用 $iscc"
& $iscc 'installer\iosloc.iss'
if ($LASTEXITCODE -ne 0) { Write-Fail "Inno Setup 编译失败"; exit 1 }
Get-ChildItem 'dist\installer\*.exe' | ForEach-Object {
Write-Ok "$($_.FullName)($([math]::Round($_.Length/1MB,1)) MB)"
}
}
}
Write-Bold "完成"
Write-Host " 绿色版:dist\iOSLocationTrack\iOSLocationTrack.exe"
if (Test-Path 'dist\installer') {
Get-ChildItem 'dist\installer\*.exe' -ErrorAction SilentlyContinue | ForEach-Object {
Write-Host " 安装包:$($_.FullName)"
}
}
Write-Host "`n 提示:exe 启动时会请求管理员权限(用于首次自动安装 Apple Mobile Device Support)。"
+106
View File
@@ -0,0 +1,106 @@
# iOS 虚拟定位轨迹工具 —— Windows 一键启动脚本
#
# 用法(在项目根目录):
# .\scripts\run.ps1 # 正常启动
# .\scripts\run.ps1 -Mock # 无设备模拟模式
# .\scripts\run.ps1 -CheckDriver # 只检查 Apple Mobile Device Support
# .\scripts\run.ps1 -InstallDriver # 检查并安装 Apple 驱动
# .\scripts\run.ps1 -NoBrowser # 不自动打开界面
#
# 如果提示"禁止运行脚本",先执行:
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
[CmdletBinding()]
param(
[switch]$Mock,
[switch]$CheckDriver,
[switch]$InstallDriver,
[switch]$NoBrowser,
[switch]$NoDriverCheck,
[int]$Port = 0,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Rest
)
$ErrorActionPreference = 'Stop'
function Write-Info { param($m) Write-Host " $m" }
function Write-Bold { param($m) Write-Host $m -ForegroundColor White }
function Write-Warn { param($m) Write-Host " $m" -ForegroundColor Yellow }
function Write-Fail { param($m) Write-Host " $m" -ForegroundColor Red }
function Write-Ok { param($m) Write-Host " $m" -ForegroundColor Green }
$Root = Split-Path -Parent $PSScriptRoot
Set-Location $Root
$VenvPy = Join-Path $Root '.venv\Scripts\python.exe'
# 国内网络默认走阿里云镜像(可用 IOSLOC_PIP_INDEX 覆盖)
$PipIndex = if ($env:IOSLOC_PIP_INDEX) { $env:IOSLOC_PIP_INDEX } else { 'https://mirrors.aliyun.com/pypi/simple/' }
Write-Bold "iOS 虚拟定位轨迹工具(Windows)"
Write-Info "目录:$Root"
# ---------- 1. Python ----------
if (-not (Test-Path $VenvPy)) {
Write-Info "首次运行:创建虚拟环境…"
$python = $null
foreach ($candidate in @('py -3.12', 'py -3.13', 'py -3.11', 'python')) {
$parts = $candidate.Split(' ')
$exe = Get-Command $parts[0] -ErrorAction SilentlyContinue
if ($exe) { $python = $candidate; break }
}
if (-not $python) {
Write-Fail "未找到 Python 3.11+。请先安装:https://www.python.org/downloads/windows/"
Write-Fail "安装时请勾选「Add python.exe to PATH」。"
exit 1
}
Write-Info "使用 $python 创建虚拟环境…"
Invoke-Expression "$python -m venv `"$Root\.venv`""
if (-not (Test-Path $VenvPy)) { Write-Fail "虚拟环境创建失败"; exit 1 }
}
Write-Info "Python:$(& $VenvPy -V 2>&1)"
# ---------- 2. 依赖 ----------
$needInstall = $false
& $VenvPy -c "import pymobiledevice3" 2>$null
if ($LASTEXITCODE -ne 0) { $needInstall = $true }
if ($needInstall) {
Write-Bold "首次运行:安装依赖(约 1-3 分钟,走国内镜像)"
& $VenvPy -m pip install --quiet --disable-pip-version-check -i $PipIndex pymobiledevice3
if ($LASTEXITCODE -ne 0) {
Write-Warn "国内镜像失败,改用官方源重试…"
& $VenvPy -m pip install --quiet --disable-pip-version-check pymobiledevice3
if ($LASTEXITCODE -ne 0) {
Write-Fail "依赖安装失败。请手动执行:"
Write-Fail " $VenvPy -m pip install -i $PipIndex pymobiledevice3"
exit 1
}
}
Write-Ok "依赖安装完成"
}
Write-Info "pymobiledevice3:$(& $VenvPy -c "import pymobiledevice3;print('已就绪')" 2>$null)"
# ---------- 3. 开发者镜像素材(不入库,需要时下载)----------
$imageDmg = Join-Path $Root 'resources\ddi\personalized\Image.dmg'
if (-not (Test-Path $imageDmg)) {
Write-Warn "缺少开发者镜像 Image.dmg(约 15MB,仓库中不入库),正在下载…"
& $VenvPy (Join-Path $Root 'tools\fetch_ddi.py') --personalized-only
if (-not (Test-Path $imageDmg)) {
Write-Warn "镜像下载失败:iOS 17+ 挂载会尝试在线获取,可能较慢。"
}
}
# ---------- 4. 组装参数 ----------
$mainArgs = @((Join-Path $Root 'main.py'))
if ($Mock) { $mainArgs += '--mock' }
if ($CheckDriver) { $mainArgs += '--check-driver' }
if ($InstallDriver) { $mainArgs += '--install-driver' }
if ($NoBrowser) { $mainArgs += '--no-browser' }
if ($NoDriverCheck) { $mainArgs += '--no-driver-check' }
if ($Port -gt 0) { $mainArgs += @('--port', $Port) }
if ($Rest) { $mainArgs += $Rest }
# ---------- 5. 启动 ----------
& $VenvPy @mainArgs
exit $LASTEXITCODE