qileilove

          blog已經(jīng)轉(zhuǎn)移至github,大家請訪問 http://qaseven.github.io/

          Appium Server源碼分析之作為Bootstrap客戶端

            Appium Server擁有兩個主要的功能:
            它是個http服務(wù)器,它專門接收從客戶端通過基于http的REST協(xié)議發(fā)送過來的命令
            他是bootstrap客戶端:它接收到客戶端的命令后,需要想辦法把這些命令發(fā)送給目標(biāo)安卓機器的bootstrap來驅(qū)動uiatuomator來做事情
            通過上一篇文章《Appium Server 源碼分析之啟動運行Express http服務(wù)器》我們分析了Appium Server是如何作為一個http服務(wù)器進行工作的。那么今天我們就要分析第二點,Appium Server是怎么作為bootstrap的客戶端來向目標(biāo)安卓機器的bootstrap發(fā)送命令以驅(qū)動uiautomator框架來做事情的
            1. MVC設(shè)計模式中的Controller及路由Routing
            在我們上一篇文章描述appium server在啟動http服務(wù)器的過程中,實例化appium 服務(wù)器后,下一步就是要設(shè)置好從client端過來的請求的數(shù)據(jù)路由了:
            var main = function (args, readyCb, doneCb) {
            ...
            routing(appiumServer);
            ...
            }
            這里大家要有MVC設(shè)計模式這個背景知識,我相信大家做過界面應(yīng)用或者網(wǎng)站編程的話應(yīng)該很熟悉這種解藕降低依賴的著名設(shè)計模式,如果不清楚的話請自行百度谷歌。這里我會簡要摘錄下在我們這個http服務(wù)器中Controller扮演的角色:MVC的核心就是Controller(控制器),它負(fù)責(zé)處理http客戶端傳送過來的所有請求,并決定要將什么內(nèi)容響應(yīng)給http客戶端。但Controller并不負(fù)責(zé)決定內(nèi)容應(yīng)該如何顯示,而是將特定形態(tài)的內(nèi)容響應(yīng)給MVC架構(gòu),最后才由MVC架構(gòu)依據(jù)響應(yīng)的形態(tài)來決定如何將內(nèi)容響應(yīng)給http客戶端。如何決定響應(yīng)內(nèi)容是View的責(zé)任。
            nodejs的express架構(gòu)就是采用了MVC框架的,所以這里才有了我們的Routing,我們先找到對應(yīng)的Routing文件,然后進去看看。我們先看main.js的比較前的變量定義部分:
          var http = require('http')
          , express = require('express')
          , ...
          , routing = require('./routing.js')
          可以看到routing是在main.js所在目錄的routing.js文件里導(dǎo)出來的,我們打開該文件:
          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);
          ...
            路由一開始就指定了我們MVC的處理http客戶端過來的Controller是controller.js這個javascript腳本
            然后從上面調(diào)用穿進來的appiumServer中取出express實例并賦給rest這個變量
            然后設(shè)置gloabalBeforeFilter這個控制器來處理客戶端過來的而在這個routing文件中沒有定義的請求的情況
            在往下就是定義客戶端過來的各種請求的controller處理方法了,比如最下面那個客戶端請求對一個控件進行點擊操作。這里就不一一列舉了。這里要注意的是其中大問號的都是代表變量,真正的值是客戶端傳送過來的時候賦予的,所以解析的時候可以直接取elementId就能得到真正的值了。
            這里有一點我覺得需要跟蹤下去的是上面的controller.getGlobalBeforeFilter(appium)這個調(diào)用,因為這個方法里面設(shè)置了appium server的一個很重的成員變量:
            exports.getGlobalBeforeFilter = function (appium) {
            return function (req, res, next) {
            req.appium = appium;
            req.device = appium.device;
            ...
            };
            };
            就是把appium的device這個成員變量賦予給了nodejs提供的req這個request的device這個變量,當(dāng)前在沒有啟動一個與boostrap的session前這個值為null,但往后appium.device將會賦予android這個對象,而因為上面代碼的賦值是對象賦值,所以在javascript會是指針傳遞,那么也就是說最后appium.device被賦值了android對象就相當(dāng)于req.device被賦予了android這個對象。這個是后話,下面你會跟到這些賦值的變化的了。
           2. 創(chuàng)建Appium任務(wù)隊列Work Queue
            appium server和bootstrap的連接在什么時候開始建立呢?其實這個需要由appium client端來進行啟動。也就是說如果你只是啟動appium這個應(yīng)用的話,它是不會嘗試和目標(biāo)安卓機器的bootstrap進行連接的,而一旦我們準(zhǔn)備運行一個腳本的時候,appium cilent端就會立刻先發(fā)送一個創(chuàng)建與bootstrap回話的請求“/wd/hub/session”請求過來:
            這個appium client創(chuàng)建session的請求所帶的參數(shù)就是我們腳本中設(shè)置好的capabilities,在我的例子中是這些:
            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);
            往下我們就跟蹤下創(chuàng)建session在routing路由表里對應(yīng)的controller是怎么實現(xiàn)和bootstrap的通信的,但是其實在真正實現(xiàn)通信之前,appium需要先去初始化一個async庫的queue隊列來排隊我們需要發(fā)送到bootstrap的命令任務(wù),我們下面會一步步看這個隊列是怎么建立起來的。
            我們先找到routing中對應(yīng)的controller方法:
            rest.post('/wd/hub/session', controller.createSession);
            處理函數(shù)是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發(fā)過來的request的body,也就是上面包含我們的capabilities的那一串鍵值對組成的字符串了。然后將這些鍵值對轉(zhuǎn)換成JSON格式,最后就以這些capabilities作為參數(shù)來調(diào)用req.appium的start方法,還記得req.appium是在哪里賦值的嗎?對,就在上面初始化routing的時候調(diào)用的‘controller.getGlobalBeforeFilter“這個方法里面了,初始化成我們在啟動http服務(wù)器時創(chuàng)建的那個appium server了(如果不清楚appium server是在啟動http服務(wù)器過程中什么時候創(chuàng)建的,請查看上一篇文章)。好我們跳進該方法繼續(xù)往下看:
          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"));
          }
          };
            代碼開始就是些根據(jù)傳進來的capabilites參數(shù)初始化一個Capabilities對象之類的,這里Capabilities這個類值得一提的地方是它定義了一系列的capability,其中有一類是我們在測試腳本中必須填寫的:
            var requiredCaps = [
            'platformName'
            , 'deviceName'
            ];
            也就是說其他的capability我們在腳本中可以根據(jù)需求取配置填寫,但是這兩個是必須的,硬性要求的。其實根據(jù)我對現(xiàn)有源碼的研究,在安卓上面只有platformName是必須的,deviceName只有在ios上面才會用到,只是為了保持一致性,測試安卓時還是需要傳進來而已,但是無論你設(shè)置什么值都沒有影響。
            好,我們繼續(xù)往下看,Appium類的start方法在實例化好Capabilities類后,往下有幾步非常重要:
            第一步:通過調(diào)用configure方法來初始化Android設(shè)備類,Android設(shè)備類的實例維護的Appium Work Queue
            第二步:通過調(diào)用invoke方法建立好uiautomator類與bootstrap的連接
            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首先會去調(diào)用Appium類的getDeviceType這個方法,而這個方法最終又會去調(diào)用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就三個,所以我們在測試腳本設(shè)置capabilities選項的時候別填錯了:
            ios
            android
            firefox
            最終返回的device定義如下,其實就是一些對應(yīng)的字串:
            var DT_IOS = "ios"
            , DT_SAFARI = "safari"
            , DT_ANDROID = "android"
            , DT_CHROME = "chrome"
            , DT_SELENDROID = "selendroid"
            , DT_FIREFOX_OS = "firefoxos";
            但是別小看這些字串,我們下面會看到就是通過他們來實例化對應(yīng)的設(shè)備類的。
            在獲得deviceType后,configure方法下一個重要的步驟就是去根據(jù)這個deviceType字串去調(diào)用getNewDevice這個方法獲得或者叫做創(chuàng)建一個對應(yīng)的設(shè)備對象了:
          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這個變量是通過匿名函數(shù)返回的一個別的地方export出來的一個對象,比如以DT_ANDROID這個deviceType為例子,它返回的是Android,而Android的定義是:
            , Android = require('./devices/android/android.js')
            而android.js導(dǎo)出來的其實就是Android這個類:
            var Android = function () {
            this.init();
            };
            ...
            module.exports = Android;
            最終getNewDevice這個方法通過new DeviceClass()對設(shè)備類進行實例化,事實上就是相當(dāng)于new Android(),在我們這個例子中。那么在實例化Android這個設(shè)備類的時候其構(gòu)造函數(shù)調(diào)用init方法又做了什么事情呢?
            Android.prototype.init = function () {
            ...
            this.args.devicePort = 4724;
            ...
            this.initQueue();
            ...
            this.adb = null;
            ...
            this.uiautomator = null;
            ...
            }
            Android類的init方法會初始化一大堆成員變量,在這里我們列出幾個我們這篇文章需要關(guān)注的:
            args.devicePort:指定我們pc端forward到bootstrap的端口號4724
            adb:Android Debug Bridge實例,初始化為null,往后很進行設(shè)置
            uiautomator:初始化為空,往后會設(shè)置成uiautomator類的實例,轉(zhuǎn)本處理往bootstrap發(fā)送接收命令的事情
            當(dāng)中還調(diào)用了一個initQueue方法來把Appium的Work Queue給初始化了,這個Work Queue其實就是nodejs的async這個庫的queue這個流程控制對象。首先,我們要搞清楚我們?yōu)槭裁葱枰玫竭@個queue呢?我們知道nodejs是異步執(zhí)行框架的,如果不做特別的處理的話,我們一下子來了幾個命令如“1.點擊按鈕打開新頁面;2.讀取新頁面讀取目標(biāo)控件內(nèi)容和預(yù)期結(jié)果比較”,那么nodejs就會兩個命令同時執(zhí)行,但不保證誰先占用了cpu完成操作,那么問題就來了,如果在準(zhǔn)備執(zhí)行1之前,cpu調(diào)度切換到2,那么我們的腳本就會失敗,因為我們1還沒有執(zhí)行完,新頁面還沒有打開!
            而async這個庫的不同對象就是專門針對這些問題提供的解決辦法,比如waterfals,auto,serials和queue等,其他的我暫時沒有碰到,所以不清楚,至于queue是怎么運作的,我們摘錄下網(wǎng)上的一個解析:
            queue: 是一個串行的消息隊列,通過限制了worker數(shù)量,不再一次性全部執(zhí)行。當(dāng)worker數(shù)量不夠用時,新加入的任務(wù)將會排隊等候,直到有新的worker可用。
            這里worker決定了我們一次過能并行處理queue里面的task的數(shù)量,我們看下Appium的Work Queue的worker是多少:
          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);
          };
            從倒數(shù)第2行我們可以看到worker是1,也就是一次過appium只會處理一個task,其他push進去的task只能等待第一個task處理完。那么這樣就清楚了,我們剛才提到的兩個命令,只要保證1先于2入隊列,那么在異步執(zhí)行的nodejs框架中就能保證1會先于2而執(zhí)行。
           說到執(zhí)行,其實就是初始化queue的第一個匿名函數(shù)的參數(shù),而第二個參數(shù)就是上面提到的worker的數(shù)量了,那我們繼續(xù)看下這個執(zhí)行函數(shù)是怎么執(zhí)行的。
            首先它會從push進來的task中取出action和params兩個參數(shù)(其實這兩個就是要一個命令的主要組成部分),我們在第4小節(jié)會描述一個task是怎么push進來的
            然后到最重要的一行代碼就是調(diào)用了uiautomator的sendAction方法,當(dāng)然這里我們還在初始化階段,所以并沒有任務(wù)可以執(zhí)行。我們在第4小節(jié)會描述action是怎么發(fā)送出去的
            那么到現(xiàn)在為止Appium在調(diào)用start方法啟動時的第一步configure算是完成了,往下就要看第二步,
            3. 建立Appium Server和Bootstrap的連接
            我們先進入Appium類的invoke這個方法,這個方法是在第2節(jié)初始化Appium Work Queue等configuration成功的基礎(chǔ)上才會執(zhí)行的。
          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是啟動連接上設(shè)備后的回調(diào),重要的是最后面的一行,從上一節(jié)我們知道appium現(xiàn)在保存的設(shè)備類其實已經(jīng)是Android類了,它調(diào)用device的start其實就是調(diào)用了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本身的源碼里面實現(xiàn)的,調(diào)用的是另外一個叫"appium-adb"的庫,我手頭沒有源碼,所以就不去看它了,但是不用看我都猜到是怎么回事,無非就是像本人以前分析《MonkeyRunner源碼分析之啟動》時分析chimpchat一樣,把adb給封裝一下,然后提供一些額外的方便使用的方法出來而已
            創(chuàng)建uiautomator這個底層與bootstrap和目標(biāo)機器交互的類的實例,既然需要和目標(biāo)機器交互,那么剛才的adb時必須作為參數(shù)傳進去的了。大家還記得上面提到的在初始化Android這個設(shè)備類的時候uiautomator這個變量時設(shè)置成null的吧,其實它是在這個時候進行實例化的。這里有一點需要注意的是systemPort這個參數(shù),appium server最終與bootstrap建立的socket連接的端口就是它,現(xiàn)在傳進來的就是
            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的數(shù)量來看同時執(zhí)行多少個task的,而series時完全按順序執(zhí)行的,所以叫做series
            這個series 要做的事情就多了,主要就是真正運行時的環(huán)境準(zhǔn)備,比如檢查目標(biāo)及其api的level是否大于17,安裝測試包,bootstrap端口轉(zhuǎn)發(fā),開始測試目標(biāo)app等,如果每個都進行分析的話大可以另外開一個系列了。這個不是不可能,今后看我時間吧,這里我就分析跟我們這個小節(jié)密切相關(guān)的uiautomator.start這個方法,其實其他的大家大可以之后自行分析。
            往下分析之前大家要注意bind的參數(shù)
            this.uiautomator.start.bind(this.uiautomator),
            大家可以看到bind的參數(shù)是當(dāng)前這個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給啟動起來,啟動的詳細(xì)流程這里就不談了,大家可以看本人之前的文章《Appium Android Bootstrap源碼分析之啟動運行》
            啟動好bootstrap之后,下面就會設(shè)置相應(yīng)的事件處理函數(shù)來處理adb啟動的bootstrap在命令行由標(biāo)準(zhǔn)輸出,錯誤,以及退出的情況,注意這些輸出跟bootstrap執(zhí)行命令后返回給appium server的輸出是沒有半毛錢關(guān)系的,那些輸出是通過socket以json的格式返回的。
            這里我們看下outputStreamHandler這個收到標(biāo)準(zhǔn)輸出時的事件處理函數(shù),這個函數(shù)肯定是會觸發(fā)的,你可以去adb shell到安卓機器上面通過uiatuomator命令啟動bootstrap看下,你會看到必然會有相應(yīng)的標(biāo)準(zhǔn)輸出打印到命令行中的,只是在這里標(biāo)準(zhǔn)輸出被這個函數(shù)進行處理了,而不是像我們手動啟動那樣只是打印到命令行給你看看而已。
            UiAutomator.prototype.outputStreamHandler = function (output) {
            this.checkForSocketReady(output);
            this.handleBootstrapOutput(output);
            };
            很簡短,僅僅兩行,第二行就是剛才說的去處理bootstrap本應(yīng)打印到命令行的其他標(biāo)準(zhǔn)輸出。而第一行比較特殊,注意它的輸入?yún)?shù)也是ouput這個標(biāo)準(zhǔn)輸出數(shù)據(jù),我們進去看看它要干嘛:
            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 的時候標(biāo)準(zhǔn)輸出有沒有"Appium Socket Server Ready"這個字符串,有就代表bootstrap已經(jīng)啟動起來了,沒有的話這個函數(shù)就直接finish了。
            為了印證,我們可以在adb shell中直接輸入下面命令然后看該字串是否真的會出現(xiàn):
            uiautomator runtest AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap
            輸入如下,果不其然:
            在確保bootstrap已經(jīng)啟動起來后,下一個動作就是我們這一小節(jié)的目的,通過調(diào)用nodejs標(biāo)準(zhǔn)庫的方法net.connect去啟動與bootstrap的socket連接了:
            this.socketClient = net.connect(this.systemPort, function () {
            UiAutomator實例,也就是Android類實例的uiautomator對象,所擁有的socketClient就是appium server專門用來與bootstrap通信的實例。這里的this.systemPort就是4724,至于為什么,我就不回答了,大家去再跟蹤細(xì)點就知道了
          4. 往Bootstrap發(fā)送命令
            既然任務(wù)隊列已經(jīng)初始化,與boostrap通信的socket也建立妥當(dāng)了,那么現(xiàn)在就是時候看一個實例來看下appium server是如何在接受到appium client的rest命令后,往bootstrap那邊去灌命令的了。
            開始之前我們先看下debug log,看下appium client端發(fā)送過來的命令及相關(guān)的輸出:
            我們可以看到client端發(fā)送過來的命令是:
            info: --> POST /wd/hub/session/ae82c5ae-76f8-4f67-9312-39e4a52f5643/element/2/click {"id":"2"}
            那么我們參照路由routing表查找到對應(yīng)的處理controller:
            rest.post('/wd/hub/session/:sessionId?/element/:elementId?/click', controller.doClick);
            對應(yīng)的處理方法是controller.doClick。注意這里前面一部分是http request的body,后面一部分是params。
            這里我們可以回顧下上面說過的打問好的是變量,會用真實的值進行替代的,如在我們的例子中:
            sessionId:ae82c5ae-76f8-4f67-9312-39e4a52f5643
            elementId: 2
            從這些參數(shù)我們可以知道appium client端需要appium server幫忙處理的事情是:請在當(dāng)前這個session中點擊當(dāng)前界面的bootstrap那邊的控件哈稀表鍵值為2的控件(至于控件哈稀表這個概念如果不清楚的勞煩你先去看下本人的這篇文章《Appium Android Bootstrap源碼分析之控件AndroidElement》)
            ok,往下我們進入這個doClick方法進行分析:
            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是怎么回事做什么用的就夠了,這個不影響我們?nèi)ダ斫鈇ppium server的運行原理。
            第二行去調(diào)用nodejs提供的http request對象的device的click方法,這個device是什么呢?這個大家不記得的話請翻看第1節(jié)我們在初始化路由表的時候調(diào)用的getGlobalBeforeFilter方法中是把Appium對西那個的device對象賦予給了了這個request.device對象的:
            req.device = appium.device;
            而appium.device對象又是在第2節(jié)在start Appium實例時通過其configuration等一系列調(diào)用中初始化的,最終在我們安卓環(huán)境中就是初始化成Android這個設(shè)備類的實例,而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);
            };
            只有一行,調(diào)用的是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的處理已經(jīng)完成任務(wù)了,因為這個queue不是由click的相關(guān)處理controller來控制的,它只是負(fù)責(zé)把這個任務(wù)加入到隊列,而真正去隊列取出任務(wù)進行執(zhí)行的是我們上面第2節(jié)最后面提到的初始化async queue的時候的第一個參數(shù),而那個參數(shù)是個匿名函數(shù),沒當(dāng)有一個task進入隊列需要執(zhí)行之前都會去調(diào)用這個方法,我們回顧下:
          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相關(guān)鍵值,在我們這個例子中就是:
            action:"element:click"
            params:"{elementId:2"}
            然后調(diào)用uiatutomator的sendAction方法,并把以上兩個參數(shù)給傳進去:
            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);
            };
            將參數(shù)組合成以下并傳給sendCommand:
            參數(shù)1:‘action’
            參數(shù)2:'{action:action,parames:{elementId:2}'
            進入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);
          }
          ...
          }
            根據(jù)傳進來的參數(shù),最終組合成以下參數(shù)通過第3節(jié)初始化好的與bootstrap進行socket通信的socketClient來往bootstrap灌入命令:
            {"cmd":"action","action":element:click","params":{"elementId":"2"}}
            大家對比下圖看下高亮圈住的最后一行由bootstrap打印出來的接收到的json命令字串,它們是絕對吻合的:
            往下的事情就由bootstrap進行處理了,至于不清楚bootstrap怎么處理這些命令的,請查看我上一個bootstrap源碼分析系列。
            5. 小結(jié)
            通過本文我們了解了appium server作為bootstrap的客戶端,同時又作為appium client的服務(wù)器端,是如何處理從client來的命令然后組建成相應(yīng)的json命令字串發(fā)送到bootstrap來進行處理的:
            初始化REST路由表來接收appium client過來的REST命令調(diào)用并分發(fā)給相應(yīng)的controller方法來進行處理
            appium client發(fā)送命令之前會先觸發(fā)一個創(chuàng)建session的請求,在路由表表現(xiàn)為“rest.post('/wd/hub/session', controller.createSession);”。對應(yīng)的controller處理方法會開始初始化與bootstrap的連接
            創(chuàng)建的session的過程會調(diào)用Appium類的start方法然后機型一系列的動作:
            初始化設(shè)備類Android
            用Async庫的queue對象初始化Appium的Work Queue
            創(chuàng)建adb實例并啟動adb
            安裝測試apk
            通過adb啟動bootstrap
            建立Appium Server和bootstrap的socket連接
            ...,等等等等
            創(chuàng)建好連接后Appium Server就會接收client過來的命令,比如click,然后對應(yīng)的controller處理方法就會建立命令對應(yīng)的task并把它push到async queue里面
            一旦有新task進入到async queue,對應(yīng)的回調(diào)函數(shù)就會觸發(fā),開始往已經(jīng)建立的與bootstrap的socket連接發(fā)送json字串命令

          posted on 2014-12-23 00:14 順其自然EVO 閱讀(3504) 評論(0)  編輯  收藏 所屬分類: 測試學(xué)習(xí)專欄

          <2014年12月>
          30123456
          78910111213
          14151617181920
          21222324252627
          28293031123
          45678910

          導(dǎo)航

          統(tǒng)計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 青浦区| 新疆| 焦作市| 莱州市| 巧家县| 敦煌市| 深圳市| 西青区| 屏边| 敦化市| 灵璧县| 和田市| 阳西县| 祁连县| 睢宁县| 云梦县| 财经| 敖汉旗| 红安县| 枣庄市| 台中市| 广灵县| 交口县| 肃宁县| 固原市| 阿荣旗| 科技| 浦北县| 绩溪县| 敖汉旗| 江孜县| 永善县| 邵阳市| 仙游县| 北宁市| 平武县| 屏东县| 东山县| 简阳市| 收藏| 仙居县|