Appium Understand Appium

myersguo · 2017年02月09日 · 最后由 阿gu 回复于 2017年05月26日 · 634 次阅读
本帖已被设为精华帖!

说在前面

本文针对 appium(version:1.6.4-beta)「比较粗糙」的介绍了下它的源码的实现流程。难免有不妥支出,有任何问题,可直接沟通交流。

(本文中没有相应的测试)

Appium 的架构

appium

appium

appium

起步

下载 appium 的源码,并安装依赖:

git clone https://github.com/appium/appium.git
npm install

启动 appium:node .

这个启动命令实际是执行的:node build\main.js(package.json 中指定了 main 入口):

...
 "main": "./build/lib/main.js",
  "bin": {
    "appium": "./build/lib/main.js"
  },
...

/build/main.js 是由/lib/main.js 经 babel 翻译后的结果,所以,我们来看下/lib/main.js 来理解 appium 的流程。

(备注:由于 appium 源码执行都是执行的编译后的方法,即 build 目录下,因此如果你想要调试进行测试,需要在各个模块 build 目录下更改调试,如果更改源码,需要 gulp transpile 进行编译)

appium server

appium uml

appium server 端实现了 HTTP REST API 接口,将 client 端发来的 API 请求,解析,发送给执行端。apium server,以及其他的 driver(android,ios)都实现了 basedriver 类。basedriver 定义了 session 的创建,命令的执行方式 (cmd 执行)。

appium server(appium driver) 大致的流程为:

  • 解析命令行参数
  • 注册路由方法
  • 解析路由

我们看一下appium server 的源码实现。

import { server as baseServer } from 'appium-base-driver';
import getAppiumRouter from './appium';
...

async function main (args = null) {
  //解析参数
  let parser = getParser();
  let throwInsteadOfExit = false;
  if (args) {
    args = Object.assign({}, getDefaultArgs(), args);
    if (args.throwInsteadOfExit) {
      throwInsteadOfExit = true;
      delete args.throwInsteadOfExit;
    }
  } else {
    args = parser.parseArgs();
  }
  await logsinkInit(args);
  await preflightChecks(parser, args, throwInsteadOfExit);
  //输出欢迎信息
  await logStartupInfo(parser, args);
  //注册接口路由,参见(appium-base-driver\lib\jsonwp\Mjsonwp.js)
  let router = getAppiumRouter(args);
  //express server类(appium-base-driver\lib\express\server.js)
  //将注册的路由,传递给express注册.
  let server = await baseServer(router, args.port, args.address);
  try {
    //是否为appium grid的node节点
    if (args.nodeconfig !== null) {
      await registerNode(args.nodeconfig, args.address, args.port);
    }
  } catch (err) {
    await server.close();
    throw err;
  }

  process.once('SIGINT', async function () {
    logger.info(`Received SIGINT - shutting down`);
    await server.close();
  });

  process.once('SIGTERM', async function () {
    logger.info(`Received SIGTERM - shutting down`);
    await server.close();
  });

  logServerPort(args.address, args.port);

  return server;
}
...
//路由
//appium.js,下面会讲解路由解析
function getAppiumRouter (args) {
  let appium = new AppiumDriver(args);
  return routeConfiguringFunction(appium);
}

URL 路由解析

上面说道,路由注册。所有支持的请求都METHOD_MAP这个全局变量里面。它是一个 path:commd 的对象集合。路由执行过程是:

  • 检查命令类型 (是否包含 session)
  • 设置代理
  • 检查命令类型与参数
  • 执行命令

我们来详细看一下 (routeConfiguringFunction) 到底做了什么 (appium-base-driver\lib\mjsonwp\Mjsonwp.js):


function routeConfiguringFunction (driver) {
  if (!driver.sessionExists) {
    throw new Error('Drivers used with MJSONWP must implement `sessionExists`');
  }
  if (!(driver.executeCommand || driver.execute)) {
    throw new Error('Drivers used with MJSONWP must implement `executeCommand` or `execute`');
  }
  // return a function which will add all the routes to the driver
  return function (app) {
    //[METHOD_MAP](#route_config),是所有的路由配置,key为path,value为method的数组
   //对METHOD_MAP的配置进行绑定
    for (let [path, methods] of _.toPairs(METHOD_MAP)) {
      for (let [method, spec] of _.toPairs(methods)) {
        // set up the express route handler
        buildHandler(app, method, path, spec, driver, isSessionCommand(spec.command));
      }
    }
  };
}
//路由绑定
//示例:
/*
  '/wd/hub/session': {
    POST: {command: 'createSession', payloadParams: {required: ['desiredCapabilities'], optional: ['requiredCapabilities', 'capabilities']}}
  },
即:
method: POST
path: /wd/hub/session
spec: array
driver: appium
*/
function buildHandler (app, method, path, spec, driver, isSessCmd) {
  let asyncHandler = async (req, res) => {
    let jsonObj = req.body;
    let httpResBody = {};
    let httpStatus = 200;
    let newSessionId;
    try {
      //判断是否是创建session命令(包含createSession,getStatus,getSessions) 
      //是否有session
      if (isSessCmd && !driver.sessionExists(req.params.sessionId)) {
        throw new errors.NoSuchDriverError();
      }
      //设置了代理则透传
      if (isSessCmd && driverShouldDoJwpProxy(driver, req, spec.command)) {
        await doJwpProxy(driver, req, res);
        return;
      }
      //命令是否支持
      if (!spec.command) {
        throw new errors.NotImplementedError();
      }
      //POST参数检查 
      if (spec.payloadParams && spec.payloadParams.wrap) {
        jsonObj = wrapParams(spec.payloadParams, jsonObj);
      }
      if (spec.payloadParams && spec.payloadParams.unwrap) {
        jsonObj = unwrapParams(spec.payloadParams, jsonObj);
      }
      checkParams(spec.payloadParams, jsonObj);
      //构造参数
      let args = makeArgs(req.params, jsonObj, spec.payloadParams || []);
      let driverRes;
      if (validators[spec.command]) {
        validators[spec.command](...args);
      }
      //!!!!执行命令
      //捕获返回值
      if (driver.executeCommand) {
        driverRes = await driver.executeCommand(spec.command, ...args);
      } else {
        driverRes = await driver.execute(spec.command, ...args);
      }

      // unpack createSession response
      if (spec.command === 'createSession') {
        newSessionId = driverRes[0];
        driverRes = driverRes[1];
      }
      ...
    } catch (err) {
      [httpStatus, httpResBody] = getResponseForJsonwpError(actualErr);
    }
    if (_.isString(httpResBody)) {
      res.status(httpStatus).send(httpResBody);
    } else {
      if (newSessionId) {
        httpResBody.sessionId = newSessionId;
      } else {
        httpResBody.sessionId = req.params.sessionId || null;
      }

      res.status(httpStatus).json(httpResBody);
    }
  };
  // add the method to the app
  app[method.toLowerCase()](path, (req, res) => {
    B.resolve(asyncHandler(req, res)).done();
  });
}

创建 session 与 executeCommand

lib\appium.js

上面说了appium server已经启动了,第一件事情,当然是创建session,然后把 command 交给这个 session 的不同 driver 去执行了。

appium 先根据 caps 进行 session 创建(getDriverForCaps),然后保存 InnerDriver 到当前 session,以后每次执行命令 (executeDCommand) 会判断是否为 appiumdriver 的命令,不是则转给相应的 driver 去执行命令 (android,ios 等)。

async createSession (caps, reqCaps) {
  caps = _.defaults(_.clone(caps), this.args.defaultCapabilities);
  let InnerDriver = this.getDriverForCaps(caps);
  this.printNewSessionAnnouncement(InnerDriver, caps);

  if (this.args.sessionOverride && !!this.sessions && _.keys(this.sessions).length > 0) {
    for (let id of _.keys(this.sessions)) {
      log.info(`    Deleting session '${id}'`);
      try {
        await this.deleteSession(id);
      } catch (ign) {
      }
    }
  }

  let curSessions;
  try {
    curSessions = this.curSessionDataForDriver(InnerDriver);
  } catch (e) {
    throw new errors.SessionNotCreatedError(e.message);
  }

  let d = new InnerDriver(this.args);
  let [innerSessionId, dCaps] = await d.createSession(caps, reqCaps, curSessions);
  this.sessions[innerSessionId] = d;
  this.attachUnexpectedShutdownHandler(d, innerSessionId);
  d.startNewCommandTimeout();

  return [innerSessionId, dCaps];
}
  async executeCommand (cmd, ...args) {
  if (isAppiumDriverCommand(cmd)) {
    return super.executeCommand(cmd, ...args);
  }

  let sessionId = args[args.length - 1];
  return this.sessions[sessionId].executeCommand(cmd, ...args);
}

在 basedriver 中executeDCommand其实是调用类的cmd定义的方法。

android 执行命令

我们以uiautomator2(\appium-uiautomator2-driver\build\lib) 为例看一下它的cmd执行情况。

getAttribute(appium-uiautomator2-driver\lib\commands\element.js) 为例说明:

commands.getAttribute = async function (attribute, elementId) {
  return await this.uiautomator2.jwproxy.command(`/element/${elementId}/attribute/${attribute}`, 'GET', {});
};

appium 通过 adb forward 将主机的 HTTP 请求转发到设备中

await this.adb.forwardPort(this.opts.systemPort, DEVICE_PORT);
//主机端口号:8200,8299
//设备端口号:6790
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 7 条回复 时间 点赞

未完待续?写的挺好的

#1 楼 @Lihuazhang 未完待续,先占坑。😄

恒温 将本帖设为了精华贴 02月09日 10:59

源码分析,娓娓道来,值得一读。

赞,写得不错,期待后续。

另外提醒下,iOS 的那个是老图了,底层还是 UIAutomation ,现在应该改 WebDriverAgent 了。

思路清晰,赞!期待后续内容。

写得不错啊,能清晰了解原理

方便提供个邮箱嘛?有些 appium 问题请教,谢谢

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册