iOS 真机 JS 注入 · 第 4 课
第 3 课证明了 js-shell --userspace 原理可行。但我真去跑,被结结实实绊了三跤:
参数把页面导去 404、命令拒绝在管道里跑、多网页时冒出一个方向键菜单。
这三个坑看着杂,其实是同一个道理:这工具是给"坐在终端前、会用眼睛看会用手按键的人"设计的;
你想让它自动化、脚本化,就得反过来把自己伪装成那个人。
--url 是"导航",不是"选页" —— 我把目标页搞成了 404我想调的是妙记尾窗那个网页。看到有个 --url 参数,想当然以为是"按 URL 帮我挑中那一页"。结果——
它不是"选页",是"把当前页导航到这个 URL"。我一传, 目标网页当场被导去了一个不存在的地址,整页 404。(当时对面同学直接看到"404了"。)
这坑的杀伤力在于它改坏了现场——你要调试的页面被你自己弄没了。教训有两层:
opened-tabs 把每个网页的完整 URL 备份下来。
一旦导航翻车,还能用 JS 的 location.href = 原URL 把页面导回去。先备份,再动手——这条对任何真机调试都成立。自动化第一反应是把命令和输入用管道喂进去(echo ... | js-shell)。不行:
报错 Inappropriate ioctl for device。因为 js-shell 是个交互式壳,
它要操作一个 TTY 终端设备(去设置回显、行编辑这些"终端才有"的东西)。
管道不是终端,它一上来就问终端要参数,要不到就崩。
雪上加霜:macOS 没有 timeout 命令,想给它套个超时也没趁手工具;盲发 script -q 又会吊死。
pty 伪终端 pseudo-terminal
就是"用软件伪造的一对终端"。我写了个 Python 驱动:用 pty.openpty() 造一对伪终端,
把 js-shell 的输入输出接上去——在它眼里,对面就是个真人在用终端。然后我在代码里"替人"读输出、发命令。
import pty, subprocess, os
master, slave = pty.openpty() # 造一对伪终端
p = subprocess.Popen(['pymobiledevice3','webinspector','js-shell','--userspace'],
stdin=slave, stdout=slave, stderr=slave) # 让它以为在真终端里
os.read(master, 8192) # 我这头"替人"看屏幕
os.write(master, (expr + '\n').encode()) # 我这头"替人"敲命令
完整脚本(含等待/重试/选页逻辑)是 ios-jsshell-drive2.py,全文在飞书调试脚本文档,团队可直接复用。
伪终端接通后,又发现 js-shell 的启动有两种形态,脚本必须都认:
❯ 光标的列表让你选。脚本要"替人"发下箭头(终端里就是 \x1b[B 这串字符),一行行移,直到高亮行的 URL 命中目标,再发回车。js-shell 启动时快时慢(webinspectord 被多方争用),有时干脆起不来。
对策:脚本内置 ~50 秒的滚动等待,识别到"菜单出现 / 进了 REPL / 终端警告"任一信号就往下走;起不来(NO_CHOOSER)就重跑。真机调试要默认"会 flaky",把重试写进流程。
这三个坑都不是"原理错",而是工具的人机接口假设了一个人类操作者: 它以为有人会看 URL 别导错、有人坐在真终端前、有人会用眼睛看菜单用手按方向键。自动化的本质,就是把这些"人类假设"一条条用代码补上——pty 补上"终端",箭头字符补上"按键",备份 URL 补上"人不会手滑"。原理是地图,这些是路况。
能执行 JS 了,最后一课回到目的本身:第 5 课——我们要探的 common.getSettings
是个异步桥调用(发出去要等 native 回话)。可 js-shell 每次执行是独立会话,没法在一次里等一个可能永远不回来的回调。
我怎么用"跨会话读全局变量"的两步走破这个局,并最终拿到"妙记 iOS 容器根本不回话"这个决定性的铁证。
📖 参考来源:pymobiledevice3 CLI Recipes
——opened-tabs / js-shell / --bundle-id 过滤等命令的官方用法,写脚本时对照。