Appium Appium 中 adb,uiautomator 重连机制

Kilmer · 2015年09月22日 · 最后由 Kilmer 回复于 2015年09月25日 · 3264 次阅读

背景:
脚本情景,Android 多用户切换。
问题:
当在多用户切换过程中,当前场景中的 adb 会断掉,与之对应的 Uiautomator 进程也会结束,这时脚本无法继续执行。
试想:
我们都知道 Appium 执行脚本动作的原理是,脚本端将动作数据发给 Appium Server,Server 通过 routing 找到对应的路由,然后再发给手机端的 Bootstrap.Bootstrap 中再根据

static {
    map.put("waitForIdle", new WaitForIdle());
    map.put("clear", new Clear());
    map.put("cleartext", new ClearText());
    map.put("orientation", new Orientation());
    map.put("swipe", new Swipe());
    map.put("flick", new Flick());
    map.put("drag", new Drag());
    map.put("pinch", new Pinch());
    map.put("click", new Click());
    map.put("touchLongClick", new TouchLongClick());
    map.put("touchDown", new TouchDown());
    map.put("touchUp", new TouchUp());
    map.put("touchMove", new TouchMove());
    map.put("getText", new GetText());
    map.put("setText", new SetText());
    map.put("getName", new GetName());
    map.put("getAttribute", new GetAttribute());
    map.put("getDeviceSize", new GetDeviceSize());
    map.put("scrollTo", new ScrollTo());
    map.put("find", new Find());
    map.put("getLocation", new GetLocation());
    map.put("getSize", new GetSize());
    map.put("wake", new Wake());
    map.put("pressBack", new PressBack());
    map.put("pressKeyCode", new PressKeyCode());
    map.put("longPressKeyCode", new LongPressKeyCode());
    map.put("takeScreenshot", new TakeScreenshot());
    map.put("updateStrings", new UpdateStrings());
    map.put("getDataDir", new GetDataDir());
    map.put("performMultiPointerGesture", new MultiPointerGesture());
    map.put("openNotification", new OpenNotification());
    map.put("source", new Source());
    map.put("compressedLayoutHierarchy", new CompressedLayoutHierarchy());
    map.put("lock", new Lock());
    map.put("isScreenOn", new isScreenOn());
    map.put("monkey", new Monkey());
    map.put("currentPackage", new CurrentPackage());
  }

产生对应的实际动作。这里就不讨论所有过程中整个错误返回机制了
那么在用户切换过程中,adb 断开连接就导致了最开始通过 adb shell uiautomator 启动的 bootstrap 进程退出,所以之后的动作都无法再通过 BootStrap 产生实际的动作了。

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

通过阅读源码可以发现,Appium Server 端是有重启 adb 和 uiautomator 的动作的,不过坑爹的地方是,

this.uiautomatorRestartOnExit = false;

也就说,在我遇到的场景里面,adb 重启了,但是 Uiautormator 并没有重启,对应的端口也没有重新映射,
下面看一下重启的整个代码逻辑

  1. 实例化 Android 对象 js 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 // Configure 这步骤会实例化一个Android对象,Android对象是包含Uiautomator对象,实例化Android对象时会初始化一个任务队列 this.configure(this.args, this.desiredCapabilities, function (err) { if (err) { logger.debug("Got configuration error, not starting session"); this.cleanupSession(); cb(err, null); } else { //如果实例化Android对象成功,通过invoke函数调用android.start() console.log("[ZHAOJIAJUN]INVOKE"); 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")); } };
  2. 实例化 Android 对象成功后,通过 this.invoke(cb),调用 android.start() ```js Appium.prototype.invoke = function (cb) { this.sessionId = UUID.create().hex; logger.debug('Creating new appium session ' + this.sessionId);

/注意:这里调整过,不再判断 autoLaunch 是否为 False,注释掉的部分为原始代码/
//if (this.device.args.autoLaunch === false) {
// // if user has passed in desiredCaps.autoLaunch = false
// // meaning they will manage app install / launching
// if (typeof this.device.noLaunchSetup === "function") {
// console.log("[ZHAOJIAJUN] NO LAUNCH SETUP");
// console.log("[ZHAOJIAJUN] SessionID:" + this.sessionId);
// this.device.noLaunchSetup(function (err) {
// if (err) return cb(err);
// cb(null, this.device);
// }.bind(this));
// } else {
// cb(null, this.device);
// }
//} 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)));
//}
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)));
};

3. 初始化uiautomator对象
```js
Android.prototype.start = function (cb, onDie) {
  this.launchCb = cb;
  this.uiautomatorExitCb = onDie;
  logger.info("Starting android appium");
  async.series([
    this.initJavaVersion.bind(this),
    this.initAdb.bind(this),
    this.initUiautomator.bind(this),
    this.prepareDevice.bind(this),
    this.packageAndLaunchActivityFromManifest.bind(this),
    this.checkApiLevel.bind(this),
    this.pushStrings.bind(this),
    this.processFromManifest.bind(this),
    this.uninstallApp.bind(this), //Need
    this.installAppForTest.bind(this),//Need
    this.forwardPort.bind(this),
    this.pushAppium.bind(this),
    this.initUnicode.bind(this),
    this.pushSettingsApp.bind(this),// Need
    this.pushUnlock.bind(this),//Need
    function (cb) {this.uiautomator.start(cb);}.bind(this),
    this.wakeUp.bind(this),//Need
    this.unlock.bind(this),//Need
    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));
};

4.在初始化 Uiautomator 的过程中,设置了 uiautomator 退出时的处理

Android.prototype.initUiautomator = function (cb) {
  if (this.uiautomator === null) {
    console.log("[ZHAOJIAJUN]Init Uiautomator");
    this.uiautomator = new UiAutomator(this.adb, this.args);
    this.uiautomator.setExitHandler(this.onUiautomatorExit.bind(this));
  }
  return cb();
};
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));
};

this.proc.on('exit', this.exitHandler.bind(this)); 从这个地方就可以看出一旦 uiautomator 的进程退出,就会调用 onUiautomatorExit 函数。

Android.prototype.onUiautomatorExit = function () {
  logger.debug("UiAutomator exited");
  logger.debug("uiautomatorIgnoreExit:" + this.uiautomatorIgnoreExit.toString())
  logger.debug("uiautomatorRestartOnExit:" + this.uiautomatorRestartOnExit.toString())
  var respondToClient = function () {
    this.stopChromedriverProxies(function () {
      this.cleanup();
      if (!this.didLaunch) {
        var msg = "UiAutomator quit before it successfully launched";
        logger.error(msg);
        this.launchCb(new Error(msg));
        return;
      } else if (typeof this.cbForCurrentCmd === "function") {
        var error = new UnknownError("UiAutomator died while responding to " +
                                      "command, please check appium logs!");
        this.cbForCurrentCmd(error, null);
      }
      // make sure appium.js knows we crashed so it can clean up
      this.uiautomatorExitCb();
    }.bind(this));
  }.bind(this);

  if (this.adb) {
    var uninstall = function () {
      logger.debug("Attempting to uninstall app");
      this.uninstallApp(function () {
        this.shuttingDown = false;
        respondToClient();
      }.bind(this));
    }.bind(this);

    if (!this.uiautomatorIgnoreExit) {
      logger.debug("Not UiautomatorIgnoreExit,exe adb shell echo ping")
      this.adb.ping(function (err, ok) {
        if (ok) {
          uninstall();
        } else {
          logger.debug("echo ping error:" + err);
          this.adb.restart(function (err) {
            if (err) {
              logger.debug(err);
            }
            if (this.uiautomatorRestartOnExit) {
              logger.debug("Start to Uiautomator restart on exit")
              this.uiautomatorRestartOnExit = true;
              this.restartUiautomator(function (err) {
                if (err) {
                  logger.debug(err);
                  uninstall();
                }
              }.bind(this));
            } else {
              uninstall();
            }
          }.bind(this));
        }
      }.bind(this));
    } else {
      this.uiautomatorIgnoreExit = false;
    }
  } else {
    logger.debug("We're in uiautomator's exit callback but adb is gone already");
    respondToClient();
  }
};

从代码中可以看出,在重启 Uiautomator 的过程中,就会对 adb 的状态进行检查,然后重启。
uiautomatorIgnoreExit 这个变量源码里面是 false,但 uiautomatorRestartOnExit 这个变量里面是 false,如果不调整过来就不会重启。即使 adb 重启了也无济于事。

共收到 4 条回复 时间 点赞

弱弱地问下,"多用户切换" 是啥概念,为何会导致 adb 重连?

#1 楼 @chenhengjie123

Android L 加入了多用户的概念。

至于 adb 重连和当前 Bootstrap 挂掉的原因,不是很清楚,毕竟属于 Android 开发的范畴了 。

只是从 Appium 维度来解决这个问题 .

#2 楼 @kilmer 原来如此,学习了。你们现在就加入了这方面的测试?

#3 楼 @chenhengjie123 嗯 . 需要验证切换用户前和切换用户后 一切设定和属性的正确性 .

wangst [该话题已被删除] 中提及了此贴 09月04日 13:06
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册