Windows 操控 iPhone:6 次误判与一个代理变量
Windows 操控 iPhone:6 次误判与一个代理变量
【摘要】在 Windows 上做了一键投屏 + 全量组件树的 iPhone 控制工具。启动反复失败,六轮排查全是误判,真凶是 go-ios 无视 NO_PROXY,附 A/B 实测与补丁。
一、缘起:安卓有 adb,iPhone 只有一条窄路
我有一台常年插在电脑上的 iPhone,上面装着某个应用,靠看广告、做任务攒免费流量。
安卓这边一句 adb shell input tap x y 就完事。iPhone 没有等价物——绕不开 WebDriverAgent(WDA):它本质是个跑在手机上的 XCTest 测试包,通过 USB 隧道把"截图、点击、读界面"这些能力暴露成 HTTP 接口。
社区里把这条路铺平的是 SideTap(github.com/ucsandman/sidetap),底层依赖 go-ios(github.com/danielpaulus/go-ios)建隧道。我这套就是在这个组合上加了两层:给 Agent 用的命令入口,和给人看的 Inspector。
我的原话:「wda 电脑控制 iphone 你帮我做一个能够实时有线连接看屏幕,另一边显示全部组件的工具」
二、先把"看得见"做出来
需求很朴素:左边实时画面,右边全部无障碍组件,点组件就等于点手机。真正的难点不在界面,在两个坑。
坑一:WDA 一次只能答一个请求。 它不是 nginx,是个 XCTest 进程,一个慢查询会把 /status、/screenshot 全部堵死。第一版没有加锁,页面刷新三次直接把整个 WDA 拖死——端口在监听,请求全超时。
坑二:组件树是 token 炸弹。 全量 /source 返回的嵌套结构带 rect、traits、rawIdentifier 等二十来个字段,一次序列化上万 token。截图更贵,一张 Base64 PNG 折合约 1500 token,缩小分辨率也不省——token 是按字符算的,不是按像素。
解决办法是两层裁剪:服务端把每个节点压成 8 个短键(t/r/l/n/v/id/...),只保留"能不能点、有没有文字、在什么位置";同时给 WDA 会话设 accessibilityDeadline=2.0,超时快照直接返回,不陪着应用一起卡。
实测数据:精简后一棵 39 节点的树 218–547 ms 返回;同样的界面走全量读屏,30 步任务要吃掉约 9 万 token,精简后降到 1.7 万。这个差别决定了 Agent 是能跑完任务,还是跑到一半上下文爆掉。
三、关掉窗口就得全停
这类工具最烦的是"收尾不干净":关掉窗口,后台还挂着四个进程占着端口,下次启动直接冲突。Windows 上的解法是三保险,缺一不可。
# 1) Job Object:进程以任何方式死亡,子进程全被 Windows 终止
info.BasicLimitInformation.LimitFlags = 0x2000 # KILL_ON_JOB_CLOSE
# 2) 控制台事件:点 X 会先发 CTRL_CLOSE_EVENT,抢在被杀前做显式清理
ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_uint)
_HANDLER_REF = ROUTINE(on_ctrl) # 必须保留引用,否则被 GC 回收
kernel32.SetConsoleCtrlHandler(_HANDLER_REF, True)
# 3) atexit:正常返回时兜底
验证方式是硬杀:任务管理器直接结束启动器进程,然后 netstat 查四个端口——8788、8100、9100、28100 全部释放,tasklist 里零个残留。这一步过了,才算"一键"成立。
四、六次误判
接下来是这篇文章真正想写的部分。工具我在终端里跑得好好的,双击图标却起不来。前后六轮,每轮都有"确凿证据",每轮都错。
| 序 | 现象(真实报错) | 我当时的判断 | 实际情况 |
|---|---|---|---|
| 1 | 双击窗口一闪而过,命令行跑同一条命令正常 | 浏览器打开太早,服务还没起 | 掩盖了真问题,只是把失败藏进 timeout 里 |
| 2 | listen tcp 127.0.0.1:28100: bind: Only one usage of each socket address |
有僵尸进程占着端口 | 是重试逻辑自己起的第二个隧道撞上了第一个 |
| 3 | missing tunnel address and RSD port |
隧道建了一半就返回,属于竞态 | 隧道建成功了,是"查询"环节拿不到结果 |
| 4 | input signature expired(免费签名 7 天到期) |
重签就好 | 重签确实成功了,故障原样复现 |
| 5 | 系统 NO_PROXY 里明明有 localhost,127.0.0.1 |
代理不可能是原因 | 这个变量对 go-ios 无效 |
| 6 | failed to get tunnel info + missing tunnel address 稳定复现 |
—— | 请求被代理转发到了远端节点 |
第 4 轮最有意思。免费 Apple ID 的签名 7 天过期,这是真问题,报错信息也确实指向它。我去重签了签名,日志显示 re-signing WDA ... (nested .xctest incl.) 成功,签名状态恢复正常——然后启动照样失败,一模一样的报错。修好一个真问题而故障不变,说明它不是主因,只是恰好同时存在。
第 3 轮那次"半死隧道"的判断也值得记一笔。隧道进程明明活着,日志里有 userspace tunnel negotiated,但查询接口返回空。我据此改了重试策略:失败就全量重置,杀掉进程、等端口释放、等四秒让手机侧回收会话。改动本身没错,但它治的是症状——真凶没找到,重试再讲究也只是多试几次。
五、抓真凶:一次只改一个变量的 A/B
线索出现在隧道日志和错误日志的时间差上:
00:45:44 INFO userspace tunnel negotiated <- 隧道成了
00:45:47 WARN failed to get tunnel info <- 三秒后查不到
00:45:47 ERROR missing tunnel address and RSD port
隧道建好了,查询失败。那就只剩一个问题:这次查询到底发到哪儿去了。
于是做了三组对照,同机器、同手机、同一个可执行文件,只改环境变量:
| 环境 | tunnel ls 结果 |
|---|---|
| 干净环境(无代理变量) | 2 秒内返回条目 |
只设 HTTP_PROXY |
十秒恒为空 |
HTTP_PROXY + NO_PROXY 含 127.0.0.1 |
十秒恒为空 |
第三行是判决书。目标地址就是本机回环,NO_PROXY 白名单也写了,照样被劫——说明 go-ios 的 HTTP 客户端读代理变量,但没读例外列表。
一篇博客里说得清、当时却绕了六圈的机理:请求 GET http://127.0.0.1:28100/tunnels 被交给本地代理,代理按规则转发给远端出口节点,远端节点去连"它自己的" 127.0.0.1:28100——那是另一台机器,上面当然没有隧道服务,于是连接被拒,查询拿到空结果。
也解释了另一件怪事:为什么我在自己终端里测永远成功。我那边的代理恰好是本地中继,能正确回连本机端口,等于把错误掩盖了。同一条命令,谁的终端跑,结果不一样——那不是玄学,是环境变量。
六、补丁:一行环境清理,管住所有入口
修在模块加载处,而不是每个调用点。这样 Inspector、命令行、Agent 三个入口一次全好。
# go-ios 的 HTTP 客户端读 HTTP(S)_PROXY 但不遵守 NO_PROXY,
# 会把本机 127.0.0.1:28100 的隧道查询转发到远端节点。
# ios.exe 只跟本机回环和 usbmuxd 通信,清掉代理变量最省事。
for _v in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
os.environ.pop(_v, None)
os.environ["NO_PROXY"] = "*"
os.environ["no_proxy"] = "*"
代价要讲清楚:这条链路上唯一需要出网的操作是首次挂载开发者镜像(从 Apple CDN 下载),清掉代理后走直连。国内实测可用,而且只有系统升级后才需要重来一次。
改完跑完 562 个单元测试全绿,再用"带完整代理变量的环境"端到端启动一次——第一次尝试就起来了。这条验证很重要:必须在有故障的环境里验证修复,而不是在干净环境里欢呼。
七、可以直接抄的排查套路
1. 先分段,再猜原因。 这类链路至少四段:USB 识别 → 隧道 → WDA → 目标 App。每段都有独立探针(ios list、tunnel ls、/status、读屏),先定位死在哪段,再谈原因。我前六轮的共同错误是:拿着 A 段的报错,去修 B 段的猜测。
2. 报错文字指向的东西,未必是主因。 missing tunnel address 让我去查隧道,实际隧道好得很。经验:当两个组件之间的连接报"对端未就绪",先证明对端真的没就绪,别直接假定。
3. 环境变量一定要做 A/B。 同一个二进制、同一条命令,两个终端结果不同,九成是环境差异。最省事的做法是清空代理变量再跑一遍对比:
env -u HTTP_PROXY -u HTTPS_PROXY -u NO_PROXY ./your_binary --probe
4. 让重试"分型",别让重试"重来"。 失败原因不同,代价完全不同:链路已通、只是上层没起来,重启上层就够;链路本身可疑,必须全量重置并留出回收时间。全量重试看着稳妥,实际会把 3 秒的故障拖成 3 分钟。
八、总结
- 跨端自动化里最贵的不是代码,是"看得见"。投屏加组件树做完之后,同一个任务的调试时间从按小时算降到按分钟算。
- 排查时要敢于推翻自己上一轮的结论。我六轮里有三轮是在给上一轮的误判打补丁,越补越远。真正的转折点是把"我觉得"换成"我测一下"。
- 环境变量是最容易被忽略的故障源,尤其是会"局部生效"的那种——它让开发者的机器一切正常、换台机器就全崩,还能让报错信息指向完全无关的组件。
仓库:sidetap · go-ios · WebDriverAgent
END
- 点赞
- 收藏
- 关注作者
评论(0)