Appium @appium/execute-driver-plugin 版本演进:从 vm2 到原生 vm、bluebird 到原生 Promise 与 ESM 化
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
@appium/execute-driver-plugin是 Appium 官方插件之一,它为 Appium 服务端新增一个appium/execute_driver端点,允许客户端把一段 WebdriverIO 脚本交给服务端在独立子进程中的 Node.jsvm沙箱内执行,从而获得一定的并行化执行能力。下文以 packages/execute-driver-plugin/CHANGELOG.md 中记录的完整版本历史为主线,结合 lib/plugin.ts 与 lib/execute-child.ts 等源码,梳理该插件从 1.x 到 7.0.0 的关键变更、各次破坏性变更(Breaking Changes)的迁移要点,以及沙箱安全机制的演进过程。
插件是什么:端点、启用方式与基本用法
在进入版本史之前,先基于 README.md 与源码确认这个插件的定位,后文所有变更都发生在这个功能之上。
插件的元信息声明在 package.json 中:pluginName为execute-driver,主类为ExecuteDriverPlugin,当前仓库内版本为 7.0.0。它新增的 HTTP 端点在 lib/plugin.ts 的newMethodMap中定义:
'/session/:sessionId/appium/execute_driver': { POST: { command: 'executeDriverScript', payloadParams: {required: ['script'], optional: ['type', 'timeout']}, }, }即请求体中script(脚本字符串)必填,type(脚本类型,当前仅支持webdriverio)与timeout(超时毫秒数)可选。源码中DEFAULT_SCRIPT_TIMEOUT_MS定义为1000 * 60 * 60,即默认超时 1 小时。
安装与启动方式(继承自 README):
appium plugin install execute-driver由于脚本本质是任意 JavaScript,该插件属于不安全特性,必须显式激活插件并显式放行不安全特性标志:
appium --use-plugins=execute-driver --allow-insecure=<driver>:execute_driver_script这个强制开关在源码 lib/plugin.ts 中通过driver.isFeatureEnabled('execute_driver_script')校验,未开启时直接抛出包含--allow-insecure=${automationName}:execute_driver_script提示的错误。E2E 测试 test/e2e/plugin.e2e.spec.ts 也专门验证了“未设置--allow-insecure时命令必须失败”这一行为。
调用示例(README 原样保留):
// JavaScript (WebdriverIO) const script = `return await driver.getTimeouts();`; const {result, logs} = await driver.executeDriverScript(script); // 'result' 是脚本的返回值;'logs' 是脚本执行期间 console 的全部输出自 6.0.0 起脚本内还可用setTimeout/clearTimeout实现无条件延时:
// 大约执行 1 秒 const script = `return await new Promise((resolve) => setTimeout(resolve, 1000));`;版本里程碑总览
CHANGELOG 记录该包自 1.0.2(2022-04-20)发布以来的全部版本。绝大多数 minor/patch 版本是依赖(webdriverio、vm2)滚动更新或 monorepo 联动升版(“Version bump only”),真正影响使用方的里程碑如下表(全部取自 CHANGELOG.md):
| 版本 | 日期 | 类型 | 要点 |
|---|---|---|---|
| 2.0.0 | 2022-05-31 | 破坏性 | 改为 peer dependency,必须与appium一起安装 |
| 3.0.0 | 2022-12-14 | 破坏性 | Node 版本范围收紧为^14.17.0 \|\| ^16.13.0 \|\| >=18.0.0 |
| 3.0.35 | 2024-09-26 | 关键修复 | 用 Node 内置vm模块替换vm2 |
| 4.0.0 | 2025-01-02 | 破坏性 | WebdriverIO 升级到 v9 大版本 |
| 5.0.0-rc.1 | 2025-08-14 | 破坏性 | 最低 Node.js 版本提升到 v20.19.0 |
| 5.1.0 | 2026-01-26 | 功能 | 代码迁移到 TypeScript |
| 6.0.0 | 2026-03-08 | 破坏性 | 脚本上下文中的Promise由 bluebird 换成原生 Promise |
| 6.0.2 | 2026-04-23 | 安全修复 | 保护 VM 可访问实例免受原型污染 |
| 7.0.0 | 2026-08-24 | 破坏性 | 整个包转为 ESM-only;子进程崩溃时快速失败 |
从版本节奏可以看到两条清晰的演进线:一是运行时现代化(vm2 → 内置 vm、bluebird → 原生 Promise、CommonJS → ESM),二是安全加固(原型污染防护、VM 逃逸面收敛)。下面逐条展开。
破坏性变更一:2.0.0 起必须与 appium 一同安装
CHANGELOG 中 2.0.0(2022-05-31)的 BREAKING CHANGES 说明:@appium/execute-driver-plugin现在期望与appium一起安装。这与当前 package.json 中的声明一致:
"peerDependencies": { "appium": "^3.0.0-beta.0" }也就是说该插件不会自带 Appium 依赖树,而是复用宿主appium安装。这一设计与 lib/execute-child.ts 中“重复定义元素标识键以避免重新加载庞大依赖树”的做法互为呼应——子进程侧刻意保持轻量,只在需要时动态import('webdriverio')。
破坏性变更二:3.0.35 用 Node 内置 vm 替换 vm2
这是插件历史上最重要的一次实现级变更。CHANGELOG 3.0.35(2024-09-26)记录:“replace vm2 with Node's built-in vm”。在此之前,沙箱隔离依赖第三方vm2包(3.0.12 到 3.0.14 等版本中还能看到多条update dependency vm2 to v3.9.x记录);替换后,vm2完全退出依赖列表。
当前源码 lib/execute-child.ts 直接使用内置模块执行脚本:
let result = await vm.runInNewContext( fullScript, { driver: sandboxDriver, console: sandboxConsole, setTimeout: sandboxSetTimeout, clearTimeout: sandboxClearTimeout, }, {timeout: timeoutMs, breakOnSigint: true}, );注意三个细节:
- 用户脚本被包裹为
(async () => {${script}})();的异步 IIFE,所以脚本内可以直接使用await(上面 README 示例中的return await driver.getTimeouts()即依赖这一点); - 沙箱全局只暴露 4 个宿主对象:
driver、console、setTimeout、clearTimeout; vm的timeout选项直接使用请求的timeoutMs,与父进程的超时机制形成双保险。
破坏性变更三:6.0.0 移除 bluebird,脚本只支持原生 Promise
CHANGELOG 6.0.0(2026-03-08)明确警告:脚本上下文中的Promise现在与全局Promise一致,不再是bluebird,bluebird 特有的方法不再受支持。对仍在使用Promise.props()、Promise.join()等 bluebird 专有 API 的脚本,需要改写为标准 Promise 语义。
同时,6.0.0 前后setTimeout/clearTimeout被引入脚本沙箱(见 lib/execute-child.ts),README 也把“可使用setTimeout做无条件延时”标注为“自插件版本 6.0.0 起”。从源码结构看,这两个绑定与driver、console一样都经过wrapHostBindingForVmContext包裹后再注入 VM。
破坏性变更四:4.0.0 与 WebdriverIO v9
CHANGELOG 4.0.0(2025-01-02)的 BREAKING CHANGES 是“webdriverio major version bumped to 9”。由于脚本是在 WebdriverIO 驱动器上下文中运行的,脚本内可用 API 的语义(如元素键格式、返回值结构)跟随 WebdriverIO 大版本变化。当前 package.json 锁定的版本为webdriverio 9.31.4,而整个 3.0.x 到 4.0.x 区间中密集的 “update dependency webdriverio” 条目(v8.15.x → v8.40.6 → v9.x)记录了这条升级路径。编写脚本时应以当前安装到的 WebdriverIO 版本 API 为准,不要假设 v7/v8 时代的细节。
破坏性变更五:5.0.0-rc.1 起最低 Node.js v20.19.0
CHANGELOG 5.0.0-rc.1(2025-08-14)记录了“set minimum Node.js version to v20.19.0”,同时提到扩展名前缀成为必填(Make extension name prefix mandatory)。当前仓库 package.json 的 engines 更严格:
"engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": ">=10" }即升级到 5.0.0 及以上版本时,需要先在 Appium 宿主侧确认 Node 版本满足要求,否则插件无法通过 engines 校验正常加载。
破坏性变更六:7.0.0 全面 ESM 化与快速失败
最新一版 7.0.0(2026-08-24)包含两项 BREAKING CHANGES,均可在源码中直接验证:
1. ESM-only,禁止 CommonJS require 与深层导入。当前 package.json 声明:
"type": "module", "main": "./build/lib/index.js", "exports": { ".": { "types": "./build/lib/index.d.ts", "import": "./build/lib/index.js" }, "./package.json": "./package.json" }exports字段只暴露了包根入口(及其类型声明)与package.json本身,因此require('@appium/execute-driver-plugin/lib/plugin.js')这类深层导入不再可行,消费方必须使用import或动态import()。公开入口在 lib/index.ts 中,仅导出MJSONWP_ELEMENT_KEY、W3C_ELEMENT_KEY、ExecuteDriverPlugin与类型。
2. 子进程崩溃快速失败(fail fast)。CHANGELOG 7.0.0 中的 Bug Fix “fail fast if the script process dies” 对应 lib/plugin.ts 的exit处理:子进程若在通过 IPC 回传结果之前就退出,非零退出码会立即reject(错误信息包含 exit code 与 signal),而不是继续等待最长 1 小时的脚本超时;干净的 0 退出且无 IPC 结果则被当作空成功(resolve({}))处理,同样不再悬挂。这是一个行为层面的修复:之前进程意外死亡时调用方可能长时间无响应,现在会尽快拿到明确错误。
安全演进:从原型污染防护到 VM 逃逸面收敛
CHANGELOG 6.0.2(2026-04-23)记录了 “Protect VM-accessible instances from prototype pollution”,其落地实现是 lib/vm-host-binding.ts。该文件用较长的文件头注释解释了设计动机:vm只隔离全局与字节码,注入的宿主对象仍携带主 realm 的原型链,恶意脚本可以通过...constructor.constructor之类的原型链攀爬拿到宿主Function构造器,实现 VM 逃逸。该模块的策略包括:
- 深度 Proxy:注入 VM 的每个宿主对象/函数(
driver、console等)都被递归代理,属性读取、调用结果、描述符反射的返回值统一经过wrapIfNeeded再包装; - 原型相邻键封锁:
constructor与__proto__的读取返回冻结的空原型哨兵对象,getPrototypeOf恒返回该哨兵,has隐藏这些键,setPrototypeOf直接拒绝(见 vm-host-binding.ts 的isBlockedPrototypeKey与代理get/has/getPrototypeOf陷阱); - Promise 特殊处理:原生 Promise 无法被 Proxy 包装(会破坏 V8 对
then品牌识别,WebdriverIO 会因此出错),因此对宿主 Promise 暴露一个空原型的 thenable 门面,在 resolve/reject 值回流进 VM 前先经过包装(见wrapPromiseAsThenable); - 循环引用安全:
WeakMap双向缓存保证同一宿主对象只有一个代理,且driver.m === driver.m这类身份比较保持稳定。
需要强调的是,README 与源码注释都反复声明:vm仍然不是不可信代码的完整安全边界,该插件应视为高权限特性。这也是为什么它必须通过--allow-insecure显式开启,官方只建议在受控环境中使用。
脚本执行链路:从 HTTP 请求到子进程 vm
把上述各版本累积的实现串起来,当前(7.0.0)的完整执行链路为:
- 客户端 POST
/session/:sessionId/appium/execute_driver,携带script、可选type与timeout; - lib/plugin.ts 的
executeDriverScript依次校验:功能开关execute_driver_script是否放行、scriptType是否为webdriverio、serverHost/serverPort是否可用、timeoutMs是否为数字; - 依据当前会话构造 WebdriverIO 连接参数(
sessionId、W3C 模式、移动端标记、会话 capabilities),并cp.fork('./execute-child.js')派生子进程; - 子进程 lib/execute-child.ts 以 IPC 模式运行,收到消息后动态加载
webdriverio并attach到当前会话,然后在vm.runInNewContext中执行脚本; - 结果与
error/warn/log三级日志通过 IPC 回传;父进程用Promise.race([waitForResult(), waitForTimeout()])竞争“子进程回包”与“超时”,并在finally中取消超时轮询、断开并终止子进程,确保 Appium 主进程可以优雅退出。
返回值并非原样透传:coerceScriptResult 会先把结果做一次JSON.parse(JSON.stringify(obj))净化(无法序列化的对象降级为null),再递归处理——若对象是 WebdriverIO 元素对象,只保留ELEMENT(MJSONWP)或element-6066-...(W3C)键,两种键并存时两者都保留。因此脚本返回值必须是 JSON 可编码的数据,元素引用也只以元素键形式回到客户端。
相关类型定义(IPC 消息DriverScriptMessageEvent、回包ScriptResult等)集中在 lib/types.ts。
依赖更新史与日常维护
除里程碑外,CHANGELOG 中大量条目是 Renovate 式的依赖滚动升级,阅读它们可以还原该插件两条核心依赖的演进:
- vm2:从 3.0.x 时期的 v3.9.12 一路更新到 v3.9.19(3.0.14),随后在 3.0.35 被内置
vm取代后彻底消失; - webdriverio:v7.x(2.0.x 时代)→ 3.0.17 升到 v8(含中间大量 v8.15.x~v8.40.x 的逐版本更新)→ 4.0.0 升到 v9;当前锁定在 9.31.4;
- TypeScript 工具链:5.1.0 “Migrate to typescript” 之后,6.0.7 的两条 “typescript config / Typescript references” 修复属于该迁移的收尾。
对使用者的实际含义:该包是随 Appium monorepo 以 semantic-release 自动发版的,patch 版本绝大多数是“依赖更新或联动升版”,跨 minor/major 时优先阅读 CHANGELOG 中对应的BREAKING CHANGES小节,即可判断是否需要修改脚本或宿主环境。
迁移核对清单
结合 CHANGELOG 与当前仓库内容,按版本区间的升级核对清单如下:
| 升级到 | 需要检查 |
|---|---|
| 2.0.0 | 插件与appium同处一个依赖树(peer dependency) |
| 3.0.0 | 宿主 Node 在^14.17.0 \|\| ^16.13.0 \|\| >=18.0.0范围内 |
| 3.0.35+ | 不再依赖vm2;脚本行为以 Node 内置vm语义为准 |
| 4.0.0 | 脚本改用 WebdriverIO v9 API |
| 5.0.0-rc.1+ | 宿主 Node >= 20.19.0;扩展名带前缀 |
| 6.0.0 | 脚本移除 bluebird 专有方法;可利用setTimeout/clearTimeout |
| 6.0.2+ | 若曾依赖对宿主对象原型链的访问(包括原型污染式技巧),需移除 |
| 7.0.0 | 宿主为 ESM 环境(import加载);不再深层导入包内部文件;子进程异常退出会立即报错而非等待超时 |
以上信息分别来自 CHANGELOG.md 的版本条目与 package.json、lib/plugin.ts、lib/execute-child.ts、lib/vm-host-binding.ts 的当前实现;行为层面的验证可参考 test/e2e/plugin.e2e.spec.ts 与 test/unit/vm-host-binding.spec.ts。
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考