Implementation of Appium

Posted by rpanning on Mon, 20 May 2019 23:52:39 +0200

This article is from: [TesterHome article] (https://testerhome.com/topics/7352)

In this paper, the implementation process of appium(version:1.6.4-beta) source code is introduced. Inevitably, there are inappropriate expenditures. If you have any problems, you can communicate directly.

(There is no corresponding test in this article)

Appium Architecture





start

Download appium source code and install dependencies:

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

Start appium:node.

This startup command is actually executed: node build main. JS (the main entry is specified in package. json):

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

/ build/main.js is the result of babel translation of / lib/main.js, so let's look at / lib/main.js to understand the process of appium.

(Note: Since appium source code execution is a compiled method of execution, that is, under the build directory, if you want to debug and test, you need to change the debugging in the build directory of each module, and if you change the source code, you need gulp transpile to compile)

appium server

The application server implements the HTTP REST API interface, parses and sends API requests from client to the execution. apium server and other drivers (android,ios) implement the basedriver class. Basedriver defines session creation and command execution (cmd execution).

The appium server(appium driver) process is roughly as follows:

  • Parsing command-line parameters
  • Registered Routing Method
  • Analytic routing

Let's have a look. Source code of appium server Realization.

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

async function main (args = null) {
  //Analytic parameter
  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);
  //Output welcome information
  await logStartupInfo(parser, args);
  //Register interface routing, see (appium-base-driver lib jsonwp Mjsonwp. js)
  let router = getAppiumRouter(args);
  //Express server class (appium-base-driver lib express server. js)
  //Pass the route of registration to express registration.
  let server = await baseServer(router, args.port, args.address);
  try {
    //Is it a node node of appium grid
    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;
}
...
//Route
//appium.js. Here's how routing parsing works
function getAppiumRouter (args) {
  let appium = new AppiumDriver(args);
  return routeConfiguringFunction(appium);
}

URL Routing Resolution

As mentioned above, routing registration. All requests for support are METHOD_MAP In this global variable. It is a set of path:commd objects. The routing execution process is:

  • Check the command type (whether session is included)
  • Setting agent
  • Check command types and parameters
  • Executive order

Let's take a detailed look at what routeConfiguring Function does (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) is an array of all routing configurations, with key as path and value as method.
   //Binding the configuration of 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));
      }
    }
  };
}
//Routing binding
//Example:
/*
  '/wd/hub/session': {
    POST: {command: 'createSession', payloadParams: {required: ['desiredCapabilities'], optional: ['requiredCapabilities', 'capabilities']}}
  },
Namely:
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 {
      //Determine whether to create session commands (including create Session, getStatus, getSessions)? 
      //Is there a session?
      if (isSessCmd && !driver.sessionExists(req.params.sessionId)) {
        throw new errors.NoSuchDriverError();
      }
      //If the agent is set, the transmission will be made.
      if (isSessCmd && driverShouldDoJwpProxy(driver, req, spec.command)) {
        await doJwpProxy(driver, req, res);
        return;
      }
      //Is the command supported?
      if (!spec.command) {
        throw new errors.NotImplementedError();
      }
      //POST parameter checking 
      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);
      //Structural parameters
      let args = makeArgs(req.params, jsonObj, spec.payloadParams || []);
      let driverRes;
      if (validators[spec.command]) {
        validators[spec.command](...args);
      }
      //!!!!! Execute orders
      //Capture return value
      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();
  });
}

Create session and executeCommand

lib\appium.js

As mentioned above, appium server has been started. The first thing, of course, is to create a session, and then give the command to the different driver s of the session to execute.

Appium first creates session based on caps (get Driver ForCaps), then saves InnerDriver to the current session, and then executes the command (DCommand) to determine whether it is an appium driver command or not, and then transfers it to the corresponding driver to execute the command (android,ios, etc.).

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);
}

In basedriver, executeDCommand is actually a method of calling the cmd definition of a class.

android executes commands

Let's take UI Automator 2 ( appium-ui Automator 2-driver build lib) as an example to see its implementation of cmd.

Take getAttribute (appium-uiautomator 2-driver lib commands element. js) as an example to illustrate:

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

appium forwards HTTP requests from hosts to devices via adb forward

await this.adb.forwardPort(this.opts.systemPort, DEVICE_PORT);
//Host port number: 8200,8299
//Equipment port number: 6790

Topics: Session Android Attribute git