背景:
脚本情景,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 并没有重启,对应的端口也没有重新映射,
下面看一下重启的整个代码逻辑
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"));
}
};
/注意:这里调整过,不再判断 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 重启了也无济于事。