Appium Server拥有两个主要的功能:
通过上一篇文章《Appium Server 源码分析之启动运行Express http服务器》我们分析了Appium Server是如何作为一个http服务器进行工作的。那么今天我们就要分析第二点,Appium Server是怎么作为bootstrap的客户端来向目标安卓机器的bootstrap发送命令以驱动uiautomator框架来做事情的
在我们上一篇文章描述appium server在启动http服务器的过程中,实例化appium 服务器后,下一步就是要设置好从client端过来的请求的数据路由了:
[Javascript] view plaincopynodejs的express架构就是采用了MVC框架的,所以这里才有了我们的Routing,我们先找到对应的Routing文件,然后进去看看。我们先看main.js的比较前的变量定义部分:
var http = require('http') , express = require('express') , ... , routing = require('./routing.js')
可以看到routing是在main.js所在目录的routing.js文件里导出来的,我们打开该文件:
var cOntroller= require('./controller.js'); module.exports = function (appium) { var rest = appium.rest; var globalBeforeFilter = controller.getGlobalBeforeFilter(appium); // Make appium available to all REST http requests. rest.all('/wd/*', globalBeforeFilter); routeNotYetImplemented(rest); rest.all('/wd/hub/session/*', controller.sessionBeforeFilter); rest.get('/wd/hub/status', controller.getStatus); rest.post('/wd/hub/session', controller.createSession); rest.get('/wd/hub/session/:sessionId?', controller.getSession); rest.delete('/wd/hub/session/:sessionId?', controller.deleteSession); rest.get('/wd/hub/sessions', controller.getSessions); rest.get('/wd/hub/session/:sessionId?/context', controller.getCurrentContext); rest.post('/wd/hub/session/:sessionId?/context', controller.setContext); rest.get('/wd/hub/session/:sessionId?/contexts', controller.getContexts); rest.post('/wd/hub/session/:sessionId?/element', controller.findElement); rest.post('/wd/hub/session/:sessionId?/elements', controller.findElements); rest.post('/wd/hub/session/:sessionId?/element/:elementId?/value', controller.setValue); rest.post('/wd/hub/session/:sessionId?/element/:elementId?/click', controller.doClick); ...
exports.getGlobalBeforeFilter = function (appium) { return function (req, res, next) { req.appium = appium; req.device = appium.device; ... }; };就是把appium的device这个成员变量赋予给了nodejs提供的req这个request的device这个变量,当前在没有启动一个与boostrap的session前这个值为null,但往后appium.device将会赋予android这个对象,而因为上面代码的赋值是对象赋值,所以在Javascript会是指针传递,那么也就是说最后appium.device被赋值了android对象就相当于req.device被赋予了android这个对象。这个是后话,下面你会跟到这些赋值的变化的了。
DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setCapability("deviceName","Android"); capabilities.setCapability("appPackage", "com.example.android.notepad"); capabilities.setCapability("appActivity", "com.example.android.notepad.NotesList"); driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities); }往下我们就跟踪下创建session在routing路由表里对应的controller是怎么实现和bootstrap的通信的,但是其实在真正实现通信之前,appium需要先去初始化一个async库的queue队列来排队我们需要发送到bootstrap的命令任务,我们下面会一步步看这个队列是怎么建立起来的。 我们先找到routing中对应的controller方法:
rest.post('/wd/hub/session', controller.createSession);
exports.createSession = function (req, res) { if (typeof req.body === 'string') { req.body = JSON.parse(req.body); } ... req.appium.start(req.body.desiredCapabilities, function (err, instance) { ... }它会先取得http client发过来的request的body,也就是上面包含我们的capabilities的那一串键值对组成的字符串了。然后将这些键值对转换成JSON格式,最后就以这些capabilities作为参数来调用req.appium的start方法,还记得req.appium是在哪里赋值的吗?对,就在上面初始化routing的时候调用的‘controller.getGlobalBeforeFilter“这个方法里面了,初始化成我们在启动http服务器时创建的那个appium server了(如果不清楚appium server是在启动http服务器过程中什么时候创建的,请查看上一篇文章)。好我们跳进该方法继续往下看:
Appium.prototype.start = function (desiredCaps, cb) { var cOnfigureAndStart= function () { this.desiredCapabilities = new Capabilities(desiredCaps); this.updateResetArgsFromCaps(); this.args.webSocket = this.webSocket; // allow to persist over many sessions this.configure(this.args, this.desiredCapabilities, function (err) { if (err) { logger.debug("Got configuration error, not starting session"); this.cleanupSession(); cb(err, null); } else { this.invoke(cb); } }.bind(this)); }.bind(this); if (this.sessiOnId=== null) { configureAndStart(); } else if (this.sessionOverride) { logger.info("Found an existing session to clobber, shutting it down " + "first..."); this.stop(function (err) { if (err) return cb(err); logger.info("Old session shut down OK, proceeding to new session"); configureAndStart(); }); } else { return cb(new Error("Requested a new session but one was in progress")); } };代码开始就是些根据传进来的capabilites参数初始化一个Capabilities对象之类的,这里Capabilities这个类值得一提的地方是它定义了一系列的capability,其中有一类是我们在测试脚本中必须填写的:
var requiredCaps = [ 'platformName' , 'deviceName' ];也就是说其他的capability我们在脚本中可以根据需求取配置填写,但是这两个是必须的,硬性要求的。其实根据我对现有源码的研究,在安卓上面只有platformName是必须的,deviceName只有在ios上面才会用到,只是为了保持一致性,测试安卓时还是需要传进来而已,但是无论你设置什么值都没有影响。 好,我们继续往下看,Appium类的start方法在实例化好Capabilities类后,往下有几步非常重要:
Appium.prototype.cOnfigure= function (args, desiredCaps, cb) { var deviceType; try { deviceType = this.getDeviceType(args, desiredCaps); ... } ... this.device = this.getNewDevice(deviceType); this.device.configure(args, desiredCaps, cb); ... };configure首先会去调用Appium类的getDeviceType这个方法,而这个方法最终又会去调用getDeviceTypeFromPlatform这个方法:
Appium.prototype.getDeviceTypeFromPlatform = function (caps) { var device = null; switch (caps) { case 'ios': device = DT_IOS; break; case 'android': device = DT_ANDROID; break; case 'firefoxos': device = DT_FIREFOX_OS; break; } return device; };可以看到我们支持的platform就三个,所以我们在测试脚本设置capabilities选项的时候别填错了:
var DT_IOS = "ios" , DT_SAFARI = "safari" , DT_ANDROID = "android" , DT_CHROME = "chrome" , DT_SELENDROID = "selendroid" , DT_FIREFOX_OS = "firefoxos";但是别小看这些字串,我们下面会看到就是通过他们来实例化对应的设备类的。 在获得deviceType后,configure方法下一个重要的步骤就是去根据这个deviceType字串去调用getNewDevice这个方法获得或者叫做创建一个对应的设备对象了:
Appium.prototype.getNewDevice = function (deviceType) { var DeviceClass = (function () { switch (deviceType) { case DT_IOS: return IOS; case DT_SAFARI: return Safari; case DT_ANDROID: return Android; case DT_CHROME: return Chrome; case DT_SELENDROID: return Selendroid; case DT_FIREFOX_OS: return FirefoxOs; default: throw new Error("Tried to start a device that doesn't exist: " + deviceType); } })(); return new DeviceClass(); };DeviceClass这个变量是通过匿名函数返回的一个别的地方export出来的一个对象,比如以DT_ANDROID这个deviceType为例子,它返回的是Android,而Android的定义是:
, Android = require('./devices/android/android.js')而android.js导出来的其实就是Android这个类:
var Android = function () { this.init(); }; ... module.exports = Android;最终getNewDevice这个方法通过new DeviceClass()对设备类进行实例化,事实上就是相当于new Android(),在我们这个例子中。那么在实例化Android这个设备类的时候其构造函数调用init方法又做了什么事情呢?
Android.prototype.init = function () { ... this.args.devicePort = 4724; ... this.initQueue(); ... this.adb = null; ... this.uiautomator = null; ... }Android类的init方法会初始化一大堆成员变量,在这里我们列出几个我们这篇文章需要关注的:
Android.prototype.initQueue = function () { this.queue = async.queue(function (task, cb) { var action = task.action, params = task.params; this.cbForCurrentCmd = cb; if (this.adb && !this.shuttingDown) { this.uiautomator.sendAction(action, params, function (response) { this.cbForCurrentCmd = null; if (typeof cb === 'function') { this.respond(response, cb); } }.bind(this)); } else { this.cbForCurrentCmd = null; var msg = "Tried to send command to non-existent Android device, " + "maybe it shut down?"; if (this.shuttingDown) { msg = "We're in the middle of shutting down the Android device, " + "so your request won't be executed. Sorry!"; } this.respond({ status: status.codes.UnknownError.code , value: msg }, cb); } }.bind(this), 1); };从倒数第2行我们可以看到worker是1,也就是一次过appium只会处理一个task,其他push进去的task只能等待第一个task处理完。那么这样就清楚了,我们刚才提到的两个命令,只要保证1先于2入队列,那么在异步执行的nodejs框架中就能保证1会先于2而执行。 说到执行,其实就是初始化queue的第一个匿名函数的参数,而第二个参数就是上面提到的worker的数量了,那我们继续看下这个执行函数是怎么执行的。
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)); };这个方法很长,但做的事情也很重要:
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 () {}; };
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输入如下,果不其然:
this.socketClient = net.connect(this.systemPort, function () {UiAutomator实例,也就是Android类实例的uiautomator对象,所拥有的socketClient就是appium server专门用来与bootstrap通信的实例。这里的this.systemPort就是4724,至于为什么,我就不回答了,大家去再跟踪细点就知道了
info: --> POST /wd/hub/session/ae82c5ae-76f8-4f67-9312-39e4a52f5643/element/2/click {"id":"2"}那么我们参照路由routing表查找到对应的处理controller:
rest.post('/wd/hub/session/:sessionId?/element/:elementId?/click', controller.doClick);对应的处理方法是controller.doClick。注意这里前面一部分是http request的body,后面一部分是params。 这里我们可以回顾下上面说过的打问好的是变量,会用真实的值进行替代的,如在我们的例子中:
exports.doClick = function (req, res) { var elementId = req.params.elementId || req.body.element; req.device.click(elementId, getResponseHandler(req, res)); };第一行是把client传送过来的控件在bootstrap中的控件哈稀表key解析出来,至于为什么需要传两个一样的值然后进行或,我还没有看appium client端的代码,所以这里解析不了,也许今后有时间分析client代码的话会给大家说明白。但在这里你只需要知道这个elementId是怎么回事做什么用的就够了,这个不影响我们去理解appium server的运行原理。 第二行去调用nodejs提供的http request对象的device的click方法,这个device是什么呢?这个大家不记得的话请翻看第1节我们在初始化路由表的时候调用的getGlobalBeforeFilter方法中是把Appium对西那个的device对象赋予给了了这个request.device对象的:
req.device = appium.device;而appium.device对象又是在第2节在start Appium实例时通过其configuration等一系列调用中初始化的,最终在我们安卓环境中就是初始化成Android这个设备类的实例,而Android这个类又extend了android-controller.js里面的所有方法:
... , androidCOntroller= require('./android-controller.js') ... _.extend(Android.prototype, androidController);所以最终的click落实到了android-controller.js里面也就是androidController对象的click方法:
androidController.click = function (elementId, cb) { this.proxy(["element:click", {elementId: elementId}], cb); };只有一行,调用的是proxy这个方法,跳进去:
exports.proxy = function (command, cb) { logger.debug('Pushing command to appium work queue: ' + JSON.stringify(command)); this.push([command, cb]); };所做的事情就是直接把刚才那传命令作为一个task来push到上面提到的async.queue这个Apium Work Queue里面。 其实到了这里,click的处理已经完成任务了,因为这个queue不是由click的相关处理controller来控制的,它只是负责把这个任务加入到队列,而真正去队列取出任务进行执行的是我们上面第2节最后面提到的初始化async queue的时候的第一个参数,而那个参数是个匿名函数,没当有一个task进入队列需要执行之前都会去调用这个方法,我们回顾下:
Android.prototype.initQueue = function () { this.queue = async.queue(function (task, cb) { var action = task.action, params = task.params; this.cbForCurrentCmd = cb; if (this.adb && !this.shuttingDown) { this.uiautomator.sendAction(action, params, function (response) { this.cbForCurrentCmd = null; if (typeof cb === 'function') { this.respond(response, cb); } }.bind(this)); } else { this.cbForCurrentCmd = null; var msg = "Tried to send command to non-existent Android device, " + "maybe it shut down?"; if (this.shuttingDown) { msg = "We're in the middle of shutting down the Android device, " + "so your request won't be executed. Sorry!"; } this.respond({ status: status.codes.UnknownError.code , value: msg }, cb); } }.bind(this), 1); };取得传进来的task相关键值,在我们这个例子中就是:
UiAutomator.prototype.sendAction = function (action, params, cb) { if (typeof params === "function") { cb = params; params = {}; } var extra = {action: action, params: params}; this.sendCommand('action', extra, cb); };将参数组合成以下并传给sendCommand:
UiAutomator.prototype.sendCommand = function (type, extra, cb) { ... else if (this.socketClient) { ... var cmd = {cmd: type}; cmd = _.extend(cmd, extra); var cmdJson = JSON.stringify(cmd) + "\n"; this.cmdCb = cb; var logCmd = cmdJson.trim(); if (logCmd.length > 1000) { logCmd = logCmd.substr(0, 1000) + "..."; } this.debug("Sending command to android: " + logCmd); this.socketClient.write(cmdJson); } ... }根据传进来的参数,最终组合成以下参数通过第3节初始化好的与bootstrap进行socket通信的socketClient来往bootstrap灌入命令:
作者 |
自主博客 |
微信 |
CSDN |
天地会珠海分舵 |
http://techgogogo.com |
服务号:TechGoGoGo 扫描码:
|