Appium Server源码分析之作为Bootstrap客户端

发表于:2014-12-16 14:14

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:天地会珠海分舵    来源:51Testing软件测试网采编

  说到执行,其实就是初始化queue的第一个匿名函数的参数,而第二个参数就是上面提到的worker的数量了,那我们继续看下这个执行函数是怎么执行的。
  首先它会从push进来的task中取出action和params两个参数(其实这两个就是要一个命令的主要组成部分),我们在第4小节会描述一个task是怎么push进来的
  然后到最重要的一行代码就是调用了uiautomator的sendAction方法,当然这里我们还在初始化阶段,所以并没有任务可以执行。我们在第4小节会描述action是怎么发送出去的
  那么到现在为止Appium在调用start方法启动时的第一步configure算是完成了,往下就要看第二步,
  3. 建立Appium Server和Bootstrap的连接
  我们先进入Appium类的invoke这个方法,这个方法是在第2节初始化Appium Work Queue等configuration成功的基础上才会执行的。
Appium.prototype.invoke = function (cb) {
this.sessionId = UUID.create().hex;
logger.debug('Creating new appium session ' + this.sessionId);
if (this.device.args.autoLaunch === false) {
...
} else {
// the normal case, where we launch the device for folks
var onStart = function (err, sessionIdOverride) {
if (sessionIdOverride) {
this.sessionId = sessionIdOverride;
logger.debug("Overriding session id with " +
JSON.stringify(sessionIdOverride));
}
if (err) return this.cleanupSession(err, cb);
logger.debug("Device launched! Ready for commands");
this.setCommandTimeout(this.desiredCapabilities.newCommandTimeout);
cb(null, this.device);
}.bind(this);
this.device.start(onStart, _.once(this.cleanupSession.bind(this)));
}
};
  onStart是启动连接上设备后的回调,重要的是最后面的一行,从上一节我们知道appium现在保存的设备类其实已经是Android类了,它调用device的start其实就是调用了Android实例的start,我们跳到/devices/android/android.js看下这个start做了什么:
Android.prototype.start = function (cb, onDie) {
this.launchCb = cb;
this.uiautomatorExitCb = onDie;
logger.info("Starting android appium");
if (this.adb === null) {
this.adb = new ADB(this.args);
}
if (this.uiautomator === null) {
this.uiautomator = new UiAutomator(this.adb, this.args);
this.uiautomator.setExitHandler(this.onUiautomatorExit.bind(this));
}
logger.debug("Using fast reset? " + this.args.fastReset);
async.series([
this.prepareDevice.bind(this),
this.packageAndLaunchActivityFromManifest.bind(this),
this.checkApiLevel.bind(this),
this.pushStrings.bind(this),
this.processFromManifest.bind(this),
this.uninstallApp.bind(this),
this.installAppForTest.bind(this),
this.forwardPort.bind(this),
this.pushAppium.bind(this),
this.initUnicode.bind(this),
this.pushSettingsApp.bind(this),
this.pushUnlock.bind(this),
this.uiautomator.start.bind(this.uiautomator),
this.wakeUp.bind(this),
this.unlock.bind(this),
this.getDataDir.bind(this),
this.setupCompressedLayoutHierarchy.bind(this),
this.startAppUnderTest.bind(this),
this.initAutoWebview.bind(this)
], function (err) {
if (err) {
this.shutdown(function () {
this.launchCb(err);
}.bind(this));
} else {
this.didLaunch = true;
this.launchCb(null, this.proxySessionId);
}
}.bind(this));
};
  这个方法很长,但做的事情也很重要:
  建立adb,代码跟踪进去可以见到建立adb不是在appium server本身的源码里面实现的,调用的是另外一个叫"appium-adb"的库,我手头没有源码,所以就不去看它了,但是不用看我都猜到是怎么回事,无非就是像本人以前分析《MonkeyRunner源码分析之启动》时分析chimpchat一样,把adb给封装一下,然后提供一些额外的方便使用的方法出来而已
  创建uiautomator这个底层与bootstrap和目标机器交互的类的实例,既然需要和目标机器交互,那么刚才的adb时必须作为参数传进去的了。大家还记得上面提到的在初始化Android这个设备类的时候uiautomator这个变量时设置成null的吧,其实它是在这个时候进行实例化的。这里有一点需要注意的是systemPort这个参数,appium server最终与bootstrap建立的socket连接的端口就是它,现在传进来的就是
  var UiAutomator = function (adb, opts) {
  this.adb = adb;
  this.proc = null;
  this.cmdCb = null;
  this.socketClient = null;
  this.restartBootstrap = false;
  this.onSocketReady = noop;
  this.alreadyExited = false;
  this.onExit = noop;
  this.shuttingDown = false;
  this.webSocket = opts.webSocket;
  this.systemPort = opts.systemPort;
  this.resendLastCommand = function () {};
  };
  往下我们会看到nodejs流程控制类库async的另外一个对象series,这个有别于上面用到的queue,因为queue时按照worker的数量来看同时执行多少个task的,而series时完全按顺序执行的,所以叫做series
  这个series 要做的事情就多了,主要就是真正运行时的环境准备,比如检查目标及其api的level是否大于17,安装测试包,bootstrap端口转发,开始测试目标app等,如果每个都进行分析的话大可以另外开一个系列了。这个不是不可能,今后看我时间吧,这里我就分析跟我们这个小节密切相关的uiautomator.start这个方法,其实其他的大家大可以之后自行分析。
  往下分析之前大家要注意bind的参数
  this.uiautomator.start.bind(this.uiautomator),
  大家可以看到bind的参数是当前这个Android实例的uiautomator这个对象,所以最终start这个方法里面用到的所有的this指得都是Android这个实例的uiautomator对象。
UiAutomator.prototype.start = function (readyCb) {
logger.info("Starting App");
this.adb.killProcessesByName('uiautomator', function (err) {
if (err) return readyCb(err);
logger.debug("Running bootstrap");
var args = ["shell", "uiautomator", "runtest", "AppiumBootstrap.jar", "-c",
"io.appium.android.bootstrap.Bootstrap"];
this.alreadyExited = false;
this.onSocketReady = readyCb;
this.proc = this.adb.spawn(args);
this.proc.on("error", function (err) {
logger.error("Unable to spawn adb: " + err.message);
if (!this.alreadyExited) {
this.alreadyExited = true;
readyCb(new Error("Unable to start Android Debug Bridge: " +
err.message));
}
}.bind(this));
this.proc.stdout.on('data', this.outputStreamHandler.bind(this));
this.proc.stderr.on('data', this.errorStreamHandler.bind(this));
this.proc.on('exit', this.exitHandler.bind(this));
}.bind(this));
};
  UiAutomator的实例在启动的时候会先通过传进来的adb实例spawn一个新进程来把bootstrap给启动起来,启动的详细流程这里就不谈了,大家可以看本人之前的文章《Appium Android Bootstrap源码分析之启动运行》
  启动好bootstrap之后,下面就会设置相应的事件处理函数来处理adb启动的bootstrap在命令行由标准输出,错误,以及退出的情况,注意这些输出跟bootstrap执行命令后返回给appium server的输出是没有半毛钱关系的,那些输出是通过socket以json的格式返回的。
  这里我们看下outputStreamHandler这个收到标准输出时的事件处理函数,这个函数肯定是会触发的,你可以去adb shell到安卓机器上面通过uiatuomator命令启动bootstrap看下,你会看到必然会有相应的标准输出打印到命令行中的,只是在这里标准输出被这个函数进行处理了,而不是像我们手动启动那样只是打印到命令行给你看看而已。
  UiAutomator.prototype.outputStreamHandler = function (output) {
  this.checkForSocketReady(output);
  this.handleBootstrapOutput(output);
  };
  很简短,仅仅两行,第二行就是刚才说的去处理bootstrap本应打印到命令行的其他标准输出。而第一行比较特殊,注意它的输入参数也是ouput这个标准输出数据,我们进去看看它要干嘛:
  UiAutomator.prototype.checkForSocketReady = function (output) {
  if (/Appium Socket Server Ready/.test(output)) {
  this.socketClient = net.connect(this.systemPort, function () {
  this.debug("Connected!");
  this.onSocketReady(null);
  }.bind(this));
  this.socketClient.setEncoding('utf8');
  ...
  }
  };
  它做的第一件事情就是去检查启动bootstrap 的时候标准输出有没有"Appium Socket Server Ready"这个字符串,有就代表bootstrap已经启动起来了,没有的话这个函数就直接finish了。
  为了印证,我们可以在adb shell中直接输入下面命令然后看该字串是否真的会出现:
  uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap
  输入如下,果不其然:
  在确保bootstrap已经启动起来后,下一个动作就是我们这一小节的目的,通过调用nodejs标准库的方法net.connect去启动与bootstrap的socket连接了:
  this.socketClient = net.connect(this.systemPort, function () {
  UiAutomator实例,也就是Android类实例的uiautomator对象,所拥有的socketClient就是appium server专门用来与bootstrap通信的实例。这里的this.systemPort就是4724,至于为什么,我就不回答了,大家去再跟踪细点就知道了
54/5<12345>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号