ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Appium execute-driver 插件实战:在子进程中执行 WebdriverIO 驱动脚本的原理与用法

Appium execute-driver 插件实战:在子进程中执行 WebdriverIO 驱动脚本的原理与用法 Appium execute-driver 插件实战在子进程中执行 WebdriverIO 驱动脚本的原理与用法【免费下载链接】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 仓库中的appium/execute-driver-plugin插件展开它通过新增的POST /session/:sessionId/appium/execute_driver端点让你把一段 WebdriverIO 风格的 JavaScript 脚本发送到 Appium 服务端由服务端在独立的 Node.js 子进程中执行并返回脚本结果与执行日志。读完本文你可以完成插件的安装与不安全特性开关配置、掌握executeDriverScript的完整参数与响应结构并能从源码层面理解“子进程 fork IPC Nodevm沙箱 原型链加固”这条执行链路及其安全边界。插件是什么为什么需要它Appium 的官方插件列表plugins.md中execute-driver是用于在子进程中运行驱动脚本的插件。其定位一句话概括Appium plugin for running a driver script in a child process用于在子进程中运行驱动脚本的 Appium 插件当前插件仅支持一种驱动脚本类型webdriverio。也就是说你提交的脚本必须是一段 JavaScript且执行时暴露给脚本的driver对象是一个已经附加attach到当前会话上的 WebdriverIO 驱动实例。README 给出的动机很直接把驱动脚本放到子进程中运行可以增加一层并行化parallelisation从而可能获得更快的测试执行速度。典型场景包括在单个脚本内批量调用多个命令以减少往返、执行需要多个 Appium 命令组合才能完成的检查逻辑、或者用一段 JS 快速完成会话状态诊断例如同时读取超时配置与服务器状态。需要注意的代价插件暴露的是一个任意 JavaScript 执行端点属于高度特权能力必须运行在受控环境中详见下文“安全模型”一节。安装与启动两条启动参数缺一不可安装appium plugin install execute-driver当前仓库中该插件包名为appium/execute-driver-plugin在 package.json 中通过appium.pluginName字段注册为插件名execute-driver、主类为ExecuteDriverPlugin{ appium: { pluginName: execute-driver, mainClass: ExecuteDriverPlugin } }运行环境方面该包声明engines要求 Node.js^20.19.0 || ^22.12.0 || 24.0.0、npm10并以appium ^3.0.0-beta.0作为 peerDependency因此在 Appium 3 系含 beta上使用该插件时才具备配套的环境前提。启动服务端与所有 Appium 插件一样execute-driver必须在启动 Appium 服务端时显式激活又因为输入脚本可以是任意 JavaScript它是一个不安全特性insecure feature还必须显式放行appium --use-pluginsexecute-driver --allow-insecuredriver:execute_driver_script这里driver是需要放行该特性的自动化驱动名也可用通配符*。execute_driver_script这一特性名的完整清单可以在官方“不安全特性”参考页 insecure-features.md 中找到其中明确将其归属于execute_driver插件。从源码看特性校验发生在每次命令执行时plugin.ts 中executeDriverScript会先调用driver.isFeatureEnabled(FEAT_FLAG)FEAT_FLAG即execute_driver_script未放行时直接抛出带启动示例的错误if (!driver.isFeatureEnabled(FEAT_FLAG)) { throw new Error( Execute driver script functionality is not available unless server is started with --allow-insecure including the ${FEAT_FLAG} flag, e.g., --allow-insecure${driver.opts.automationName ?? *}:${FEAT_FLAG}, ); }E2E 测试 plugin.e2e.spec.ts 也覆盖了这一行为在服务端未带--allow-insecure启动时调用driver.executeDriverScript(basicScript)断言请求会因/allow-insecure.execute_driver_script/i而被拒绝而在serverArgs: {allowInsecure: [*:execute_driver_script]}的分组中同样的调用则正常执行。executeDriverScript API端点、参数与响应端点与参数插件在 plugin.ts 中声明了方法映射static newMethodMap: MethodMapExecuteDriverPlugin { /session/:sessionId/appium/execute_driver: { POST: { command: executeDriverScript, payloadParams: {required: [script], optional: [type, timeout]}, }, }, } as const;即端点为POST /session/:sessionId/appium/execute_driver参数完整说明如下与官方 Plugin Endpoints 文档 一致| 名称 | 说明 | 类型 | 默认值 | | -- | -- | -- | -- | |script| 要执行的脚本 | string | 必填 | |type| 执行脚本的库名 | string |webdriverio| |timeout| 脚本进程的超时时间毫秒 | number |3600000|其中默认超时 1 小时由 plugin.ts 的常量给出const DEFAULT_SCRIPT_TIMEOUT_MS 1000 * 60 * 60; // default to 1 hour timeouttype目前只接受webdriverio传入其他值会在服务端抛出TypeError: Only the webdriverio script type is currently supported见 plugin.ts。timeout如果不是合法数字同样会以Timeout parameter must be a number拒绝。响应结构响应体为RunScriptResult定义在 types.ts| 名称 | 说明 | 类型 | | -- | -- | -- | |result| 脚本返回的结果可 JSON 化 | any | |logs| 脚本执行期间产生的日志含log/warn/error三个数组 | object |客户端调用示例以 WebdriverIO 客户端为例README 原文示例// JavaScript (WebdriverIO) const script return await driver.getTimeouts();; const {result, logs} await driver.executeDriverScript(script); // result 包含脚本返回的数据此处为 getTimeouts 的响应 // logs 包含脚本执行期间输出到 console 的全部内容不同 Appium 客户端的脚本执行命令语法略有差异具体请以所用客户端的文档为准。脚本内部可直接使用已附加到当前会话的driver对象、console日志函数以及setTimeout/clearTimeout自插件6.0.0起可用。README 给出的无条件延时示例// this will take around one second to execute const script return await new Promise((resolve) setTimeout(resolve, 1000));;下面结合仓库 E2E 测试 plugin.e2e.spec.ts 中的真实用例展示几类典型用法及其预期行为。1. 组合多个命令并返回复合结果const script const timeouts await driver.getTimeouts(); const status await driver.status(); return [timeouts, status]; ; const {result, logs} await driver.executeDriverScript(script); // result[0] 形如 {command: 60000, implicit: 0}result[1] 含 build 信息 // logs 为 {error: [], warn: [], log: []}测试断言了timeouts与期望值严格相等、status.build.version存在且没有任何日志输出。2. 返回元素对象含深层结构const script const el await driver.$(#Button1); return {element: el, elements: [el, el]}; ; const {result} await driver.executeDriverScript(script);脚本返回的 WebdriverIO 元素对象会被归一化为只保留元素标识键的对象。仓库中 E2E 用例断言结果为同时携带 W3C 与 MJSONWP 两种元素键的对象element-6066-11e4-a52e-4f735466cecf与ELEMENT且该归一化会递归处理嵌套对象与数组——这一点由子进程端的coerceScriptResult实现下文源码解析。3. 收集脚本日志const script console.log(foo); console.warn(bar); console.error(baz); return null; ; const {logs} await driver.executeDriverScript(script); // logs {log: [foo], warn: [bar], error: [baz]}脚本内的console并不是真正的控制台而是一个由子进程构造的“假 logger”按级别收集消息后随结果一并 IPC 回传见 execute-child.ts。4. Appium 扩展命令也可用脚本中的driver是完整的 WebdriverIO 附加实例Appium 扩展命令同样存在E2E 用例执行return typeof driver.lock;并断言结果为function说明脚本内可以直接调用 Appium 专有的扩展方法。5. 脚本内错误的处理方式当脚本内部命令失败但脚本本身继续返回时错误以 WebdriverIO 的错误对象形式回到result中。测试用例执行return await driver.$(~notfound);一个不存在的 selector随后断言result.error.error no such element、result.error.message匹配element could not be located并携带sessionId。而如果是脚本无法编译例如return {;这种语法错误则整个请求会直接 reject错误信息形如Could not execute driver script. Original error was: ... Unexpected token。6. 超时控制timeout参数毫秒决定脚本进程的最长存活时间。测试用例用 50ms 超时执行一个需要 1 秒的setTimeout脚本断言 reject 信息中包含 50 与 timeout 字样。源码解析从 HTTP 请求到子进程执行整条调用链可以分为三段服务端插件入口fork IPC 超时竞态、子进程脚本执行vm.runInNewContext 结果归一化、以及贯穿其中的原型链加固。1. 插件入口fork 子进程并通过 IPC 交换数据plugin.ts 中executeDriverScript的核心流程前置校验特性开关、scriptType只能是webdriverio、driver.serverHost/driver.serverPort必须存在子进程需要靠地址回连 Appium 服务端、timeoutMs必须是数字。构造驱动连接参数把当前会话信息打包成 WebdriverIO 的 attach 参数const driverOpts { sessionId: driver.sessionId, options: { // Appium probably wont be behind ssl locally; if it ever is, might need to // update this to provide a user configurable parameter protocol: http, hostname: driver.serverHost, port: driver.serverPort, path: driver.serverPath, }, isW3C: true, isMobile: true, capabilities: driver.caps, };从源码结构看这里硬编码了protocol: http并附注释说明若 Appium 本地部署在 SSL 之后需要改为可配置项——这也是当前实现的一个已知限制。fork 子进程通过cp.fork启动同目录下的 execute-child.js即fileURLToPath(new URL(./execute-child.js, import.meta.url))。发送参数scriptProc.send({driverOpts, script, timeoutMs})随后用Promise.race在两个 Promise 之间竞速waitForResult等待子进程通过 Node IPC 发回messagescriptProc.once(message, resolve)。若子进程以退出码 0 结束且没有回传结果按“空成功”处理resolve{}若以非 0 退出码或被信号杀死则 reject报错信息为The driver script process ended without returning a result (exit code: ..., signal: ...)。waitForTimeout基于appium/support的timing.Timer以 500ms 为间隔轮询直到拿到结果timeoutCanceled置位或超过timeoutMs超时报错Execute driver script timed out after ${timeoutMs}ms. You can adjust this with the timeout parameter.。清理finally中先取消超时轮询再disconnect与若未退出kill子进程保证无论成功还是失败子进程都不会泄漏。子进程退出码的三种结局非 0 退出码、被信号杀死、0 退出但无消息都有对应的单元测试覆盖见 plugin.spec.ts分别断言 reject/ended without returning a result/和“干净退出视为 undefined 结果”。2. 子进程vm 沙箱执行与结果归一化execute-child.ts 是子进程入口。它通过process.send与父进程保持 IPC仅在作为 fork 直接运行且处于 IPC 模式下才启用见文件末尾 163-167 行的入口判断。核心执行逻辑在runScript中构造假 console分别包装error/warn/log三个函数把参数收集进logs对象通过await import(webdriverio)的attach(driverOpts)拿到已附加到当前会话的驱动实例把脚本包成异步 IIFE 后交给 Nodevmconst fullScript (async () {${script}})();; let result await vm.runInNewContext( fullScript, { driver: sandboxDriver, console: sandboxConsole, setTimeout: sandboxSetTimeout, clearTimeout: sandboxClearTimeout, }, {timeout: timeoutMs, breakOnSigint: true}, );也就是说脚本全局环境里只有driver、console、setTimeout、clearTimeout四个对象且timeoutMs同时被传入vm.runInNewContext作为 VM 级超时最后用coerceScriptResult对返回值做安全化转换第 74-76 行。coerceScriptResultexecute-child.ts解决的是“不可信代码可能返回任何东西”的问题先做一次JSON.parse(JSON.stringify(obj))的强制 POJO 化剥掉函数、自定义对象等无法 JSON 编码的东西失败则整体降级为null并打警告日志然后递归处理若对象含有ELEMENTMJSONWP 键或element-6066-11e4-a52e-4f735466cecfW3C 键判定为元素对象只保留其中存在的元素键脚本若同时带有两种键则都保留丢弃其余字段普通对象与数组递归处理基础类型原样返回。文件顶部导出的两个常量W3C_ELEMENT_KEY与MJSONWP_ELEMENT_KEY第 15-16 行就是这一归一化使用的键名测试也直接从execute-child.js引入它们做断言。3. 安全模型vm 不是安全边界原型链加固是纵深防御README 的安全警告值得完整理解This plugin enables execution of arbitrary JavaScript code. We recommend only using this plugin in a controlled environment. Scripts run in a Node.jsvmcontext with a hardened view of the WebdriverIO driver (host-realm prototype metadata is not exposed), butvmis still not a full security boundary for untrusted code; treat this plugin as highly privileged.Node 官方立场是vm只隔离全局变量与字节码并非针对不可信代码的完整安全边界。而本仓库在此之上做了一层纵深防御核心实现在 vm-host-binding.ts 的wrapHostBindingForVmContext第 82-84 行。该模块的头部注释给出了完整的策略说明要点如下深度 Proxy 包装注入到 VM 的每个宿主对象/函数driver、console、setTimeout等都会被递归代理。属性读取get、调用结果apply/construct、描述符反射getOwnPropertyDescriptor的返回值都会再经过wrapIfNeeded保证嵌套引用永远不会以“裸的宿主可调用对象”形式出现。原型邻接键封锁constructor与__proto__的读取不落到真实对象上而是返回一个冻结的 null-prototype 哨兵SAFE_LOOKUP_TARGETgetPrototypeOf永远报告该哨兵has隐藏这些键setPrototypeOf被拒绝。这就封堵了经典的x.constructor.constructor(...)()原型链逃逸路径。稳定身份与循环安全WeakMap保证同一宿主对象只有一个代理driver.m driver.m成立且循环引用不会无限递归反向映射则用于把this/实参解包回真实宿主对象使宿主 API 收到正确的 receiver。Promise 特判原生 Promise 直接代理会破坏 V8 对内部then的识别WebdriverIO 会坏掉因此宿主 Promise 被包装成一个 null-prototype 的 thenable先转发到真实 Promise再在 VM 侧续体运行前对 resolve/reject 值做包裹。描述符双向映射对可配置自有属性返回的value/get/set均被包裹防止通过描述符提取裸宿主方法不可配置属性则按 Proxy 不变量原样返回。单元测试 vm-host-binding.spec.ts 对这条防线做了系统性验证对对象、注入的定时器、console 聚合对象、嵌套方法如driver.deleteSession分别构造d.constructor.constructor(return typeof process)()一类的逃逸尝试全部断言抛错同时保留正常功能——普通属性可读取、setTimeout仍能调度回调。即便有这层加固源码注释依然明确表态vm-host-binding.tsNode 已声明vm不是完整安全边界应把--allow-insecure…:execute_driver_script始终视为高度特权能力。这与 README 的警告一致也决定了该特性必须配合--allow-insecure逐特性放行而不能默认开启。限制与最佳实践小结结合源码与测试使用本插件时建议牢记以下约束启动双参数--use-pluginsexecute-driver与--allow-insecuredriver:execute_driver_script或*通配缺一不可前者激活插件端点后者打开execute_driver_script特性开关脚本类型目前仅支持webdriverio类型的 JS 脚本脚本内可用全局仅有driver、console、setTimeout、clearTimeout定时器自插件 6.0.0 起提供超时默认 1 小时3600000ms可通过timeout参数毫秒调整脚本卡死时子进程会被超时逻辑终止并在finally中回收返回值必须可 JSON 化返回自定义对象、函数等会被coerceScriptResult压平为 POJO 或降级为null元素对象只保留元素标识键错误语义脚本内部命令失败通常以 WebdriverIO 错误对象形式出现在result中脚本编译/运行失败或子进程异常退出则让整个请求 reject部署假设子进程回连 Appium 服务端时硬编码protocol: http见 plugin.ts 注释本地反代 SSL 的场景下从源码结构看需要自行改造安全定位该插件是任意 JS 执行端点vm 原型链加固只是纵深防御而非安全边界只应在受控环境私有 CI、内网测试集群等中启用并对调用方身份做管控。参考路径汇总插件 READMEpackages/execute-driver-plugin/README.md、插件主实现packages/execute-driver-plugin/lib/plugin.ts、子进程执行器packages/execute-driver-plugin/lib/execute-child.ts、vm 加固层packages/execute-driver-plugin/lib/vm-host-binding.ts、类型定义packages/execute-driver-plugin/lib/types.ts、端到端测试packages/execute-driver-plugin/test/e2e/plugin.e2e.spec.ts、官方端点文档packages/appium/docs/en/reference/api/plugins.md与不安全特性清单packages/appium/docs/en/reference/cli/insecure-features.md。【免费下载链接】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),仅供参考
返回列表