Merge pull request #433 from jlipps/selendroid

Selendroid support and adb.js rewrite
This commit is contained in:
Sebastian Tiedtke
2013-04-12 11:11:52 -07:00
14 changed files with 717 additions and 297 deletions
+1
View File
@@ -29,3 +29,4 @@ app/android/AndroidManifest.xml
*~
uiautomator/bootstrap/target/
org.eclipse.ltk.core.refactoring.prefs
/selendroid
+3
View File
@@ -7,3 +7,6 @@
[submodule "submodules/appium.io"]
path = submodules/appium.io
url = https://github.com/appium/appium.io.git
[submodule "submodules/selendroid"]
path = submodules/selendroid
url = https://github.com/DominikDary/selendroid.git
+2 -1
View File
@@ -72,6 +72,7 @@ Android.prototype.start = function(cb, onDie) {
var skipRelaunchOn = [
'App never showed up'
, 'Could not sign one or more apks'
, 'Could not find a connected Android device'
];
var checkShouldSkipRelaunch = function(msg) {
var skip = false;
@@ -127,7 +128,7 @@ Android.prototype.start = function(cb, onDie) {
if (this.adb === null) {
// Pass Android opts and Android ref to adb.
this.adb = adb(this.opts, this);
this.adb.start(onLaunch, onExit);
this.adb.startAppium(onLaunch, onExit);
} else {
logger.error("Tried to start ADB when we already have one running!");
}
+25 -1
View File
@@ -15,6 +15,7 @@ var routing = require('./routing')
, _ = require('underscore')
, ios = require('./ios')
, android = require('./android')
, selendroid = require('./selendroid')
, status = require("./uiauto/lib/status");
var Appium = function(args) {
@@ -108,6 +109,8 @@ Appium.prototype.getDeviceType = function(desiredCaps) {
return "ios";
} else if (desiredCaps.device.toLowerCase().indexOf('ipad') !== -1) {
return "ios";
} else if (desiredCaps.device.toLowerCase().indexOf('selendroid') !== -1) {
return "selendroid";
} else {
return "android";
}
@@ -116,6 +119,8 @@ Appium.prototype.getDeviceType = function(desiredCaps) {
return "ios";
} else if (desiredCaps.browserName.toLowerCase() === "safari") {
return "ios";
} else if (desiredCaps.browserName.toLowerCase().indexOf('selendroid') !== -1) {
return "selendroid";
} else {
return "android";
}
@@ -147,6 +152,10 @@ Appium.prototype.isAndroid = function() {
return this.deviceType === "android";
};
Appium.prototype.isSelendroid = function() {
return this.deviceType === "selendroid";
};
Appium.prototype.getAppExt = function() {
return this.isIos() ? ".app" : ".apk";
};
@@ -352,6 +361,17 @@ Appium.prototype.invoke = function() {
, fastReset: this.args.fastReset
};
this.devices[this.deviceType] = android(androidOpts);
} else if (this.isSelendroid()) {
var selendroidOpts = {
apkPath: this.args.app
, desiredCaps: this.desiredCapabilities
, verbose: this.args.verbose
, udid: this.args.udid
, appPackage: this.args.androidPackage
, appActivity: this.args.androidActivity
, appDeviceReadyTimeout: this.args.androidDeviceReadyTimeout
};
this.devices[this.deviceType] = selendroid(selendroidOpts);
} else {
throw new Error("Tried to start a device that doesn't exist: " +
this.deviceType);
@@ -359,13 +379,17 @@ Appium.prototype.invoke = function() {
}
this.device = this.devices[this.deviceType];
this.device.start(function(err) {
this.device.start(function(err, sessionIdOverride) {
me.progress++;
// Ensure we don't use an undefined session.
if (typeof me.sessions[me.progress] === 'undefined') {
me.progress--;
return;
}
if (sessionIdOverride) {
me.sessionId = sessionIdOverride;
logger.info("Overriding session id with " + sessionIdOverride);
}
me.sessions[me.progress].sessionId = me.sessionId;
me.sessions[me.progress].callback(err, me.device);
+28
View File
@@ -1,6 +1,8 @@
"use strict";
var errors = require('./errors')
, request = require('request')
, _ = require('underscore')
, logger = require('../logger').get('appium');
var UnknownError = errors.UnknownError
@@ -57,3 +59,29 @@ exports.waitForCondition = function(waitMs, condFn, cb, intervalMs) {
};
spin();
};
exports.request = function(url, method, body, contentType, cb) {
if (typeof cb === "undefined" && typeof contentType === "function") {
cb = contentType;
contentType = null;
}
if (typeof contentType === "undefined" || contentType === null) {
contentType = "application/json";
}
if (!(/^https?:\/\//.exec(url))) {
url = 'http://' + url;
}
var opts = {
url: url
, method: method
, headers: {'Content-Type': contentType}
};
if (_.contains(['put', 'post', 'patch'], method.toLowerCase())) {
if (typeof body !== "string") {
opts.json = body;
} else {
opts.body = body;
}
}
request(opts, cb);
};
+1
View File
@@ -8,6 +8,7 @@ var logger = require('../logger').get('appium')
, path = require('path')
, rimraf = require('rimraf')
, exec = require('child_process').exec
, util = require('util')
, temp = require('temp');
exports.downloadFile = function(fileUrl, cb) {
+47 -10
View File
@@ -1,18 +1,55 @@
"use strict";
var controller = require('./controller')
, request = require('./device').request
, _ = require('underscore')
, logger = require('../logger').get('appium');
var shouldProxy = function(req) {
if (req.device === null) return false;
var avoid = [
['POST', new RegExp('^/wd/hub/session$')]
, ['DELETE', new RegExp('^/wd/hub/session/[^/]+$')]
];
var method = req.route.method.toUpperCase();
var path = req.originalUrl;
var shouldAvoid = false;
_.each(avoid, function(pathToAvoid) {
if (method === pathToAvoid[0] && pathToAvoid[1].exec(path)) {
shouldAvoid = true;
}
});
return !shouldAvoid;
};
module.exports = function(appium) {
var rest = appium.rest
, inject = function(req, res, next) {
req.appium = appium;
req.device = appium.device;
logger.debug("Appium request initiated at " + req.url);
if (typeof req.body === "object") {
logger.debug("Request received with params: " + JSON.stringify(req.body));
}
next();
};
var rest = appium.rest;
var inject = function(req, res, next) {
req.appium = appium;
req.device = appium.device;
logger.debug("Appium request initiated at " + req.url);
if (typeof req.body === "object") {
logger.debug("Request received with params: " + JSON.stringify(req.body));
}
if (shouldProxy(req)) {
logger.debug("Proxying command to " + req.device.proxyHost + ":" +
req.device.proxyPort);
var url = 'http://' + req.device.proxyHost + ':' + req.device.proxyPort +
req.originalUrl;
request(url, req.route.method.toUpperCase(), req.body,
req.headers['content-type'], function(err, response, body) {
if (err) return next(err);
logger.debug("Proxied response received with status " +
response.statusCode + ": " +
JSON.stringify(body).slice(0, 1000));
res.headers = response.headers;
res.send(response.statusCode, body);
});
} else {
next();
}
};
// Make appium available to all REST http requests.
rest.all('/wd/*', inject);
+156
View File
@@ -0,0 +1,156 @@
"use strict";
var errors = require('./errors')
, adb = require('../uiautomator/adb')
, _ = require('underscore')
, request = require('./device').request
, logger = require('../logger').get('appium')
, status = require("./uiauto/lib/status")
, exec = require('child_process').exec
, fs = require('fs')
, ncp = require('ncp')
, async = require('async')
, path = require('path')
, UnknownError = errors.UnknownError;
var Selendroid = function(opts) {
this.opts = opts;
this.serverApk = null;
this.appPackage = opts.appPackage;
this.desiredCaps = opts.desiredCaps;
this.onStop = function() {};
this.selendroidSessionId = null;
this.adb = null;
this.isProxy = true;
this.proxyHost = 'localhost';
this.proxyPort = 8080;
};
Selendroid.prototype.start = function(cb) {
logger.info("Starting selendroid server");
var opts = _.clone(this.opts)
, me = this;
opts.port = this.proxyPort;
opts.devicePort = 8080;
this.adb = new adb(opts);
async.waterfall([
function(cb) { me.ensureServerExists(cb); },
function(cb) { me.adb.startSelendroid(me.serverApk, cb); },
function(res, cb) { me.waitForServer(cb); },
function(cb) { me.createSession(cb); },
], cb);
};
Selendroid.prototype.stop = function(cb) {
logger.info("Stopping selendroid server");
this.deleteSession(function(err) {
cb(err ? 1 : 0);
});
};
Selendroid.prototype.ensureServerExists = function(cb) {
logger.info("Checking whether selendroid is built for package yet");
var fileName = 'selendroid-' + this.appPackage + '.apk';
var filePath = path.resolve(__dirname, "../selendroid/selendroid-server",
"target", fileName);
fs.stat(filePath, _.bind(function(err) {
if (err) {
logger.info("Selendroid needs to be built");
return this.buildServer(cb);
}
logger.info("Selendroid server already exists, not rebuilding");
this.serverApk = filePath;
cb(null);
}, this));
};
Selendroid.prototype.buildServer = function(cb) {
logger.info("Building selendroid server for package " + this.appPackage);
var buildDir = path.resolve(__dirname, "../selendroid/selendroid-server");
var src = buildDir + "/target/selendroid-server-0.3.apk";
var dest = buildDir + "/target/selendroid-" + this.appPackage + '.apk';
var cmd = "mvn install -Dandroid.renameInstrumentationTargetPackage=" +
this.appPackage;
exec(cmd, {cwd: buildDir}, _.bind(function(err, stdout, stderr) {
if (err) {
logger.error("Unable to build selendroid server. Stdout was: ");
logger.error(stdout);
logger.error(stderr);
return cb(err);
}
logger.info("Copying selendroid server to correct destination");
ncp(src, dest, _.bind(function(err) {
if (err) {
logger.error("Error copying selendroid to destination");
return cb(err);
}
logger.info("Selendroid server copied successfully");
this.serverApk = dest;
cb(null);
}, this));
}, this));
};
Selendroid.prototype.waitForServer = function(cb) {
var waitMs = 20000
, intMs = 800
, start = Date.now();
var pingServer = _.bind(function() {
this.proxyTo('/wd/hub/status', 'GET', null, function(err, res, body) {
if (body === null || typeof body === "undefined" || !body.trim()) {
if (Date.now() - start < waitMs) {
setTimeout(pingServer, intMs);
} else {
cb(new Error("Waited " + (waitMs / 1000) + " secs for " +
"selendroid server and it never showed up"));
}
} else {
logger.info("Selendroid server is alive!");
cb(null);
}
});
}, this);
pingServer();
};
Selendroid.prototype.createSession = function(cb) {
var data = {desiredCapabilities: this.desiredCaps};
this.proxyTo('/wd/hub/session', 'POST', data, _.bind(function(err, res, body) {
if (err) return cb(err);
if (res.statusCode === 301 && body.sessionId) {
logger.info("Successfully started selendroid session");
this.selendroidSessionId = body.sessionId;
cb(null, body.sessionId);
} else {
logger.error("Selendroid create session did not work. Status was " +
res.statusCode + " and body was " + body);
cb(new Error("Did not get session redirect from selendroid"));
}
}, this));
};
Selendroid.prototype.deleteSession = function(cb) {
var url = '/wd/hub/session/' + this.selendroidSessionId;
this.proxyTo(url, 'DELETE', null, _.bind(function(err, res) {
if (err) return cb(err);
if (res.statusCode !== 200) return cb(new Error("Status was not 200"));
cb();
}, this));
};
Selendroid.prototype.proxyTo = function(endpoint, method, data, cb) {
if (endpoint[0] !== '/') {
endpoint = '/' + endpoint;
}
var url = 'http://' + this.proxyHost + ':' + this.proxyPort + endpoint;
request(url, method, data ? data : null, cb);
};
module.exports = function(opts) {
return new Selendroid(opts);
};
+1
View File
@@ -46,6 +46,7 @@
"ncp": "~0.4.2",
"swig": "~0.13.5",
"async": "~0.2.6",
"mkdirp": "~0.3.5",
"binary-cookies": "~0.1.1"
},
"scripts": {
+26 -5
View File
@@ -7,20 +7,24 @@
set -e
should_reset_android=0
should_reset_ios=0
should_reset_selendroid=0
appium_home=$(pwd)
selendroid_ver="0.3"
while test $# != 0
do
echo $1
case "$1" in
"--android") should_reset_android=1;;
"--ios") should_reset_ios=1;;
"--selendroid") should_reset_selendroid=1;;
esac
shift
done
if [[ $should_reset_android -eq 0 ]] && [[ $should_reset_ios -eq 0 ]]; then
if [[ $should_reset_android -eq 0 ]] && [[ $should_reset_ios -eq 0 ]] && [[ $should_reset_selendroid -eq 0 ]]; then
should_reset_android=1
should_reset_ios=1
should_reset_selendroid=1
fi
reset_general() {
@@ -63,12 +67,16 @@ reset_ios() {
grunt buildApp:WebViewApp
}
reset_android() {
echo "---- RESETTING ANDROID ----"
get_apidemos() {
echo "Downloading/updating AndroidApiDemos"
git submodule update --init submodules/ApiDemos
rm -rf sample-code/apps/ApiDemos
ln -s $(pwd)/submodules/ApiDemos $(pwd)/sample-code/apps/ApiDemos
ln -s $appium_home/submodules/ApiDemos $appium_home/sample-code/apps/ApiDemos
}
reset_android() {
echo "---- RESETTING ANDROID ----"
get_apidemos
echo "Building Android bootstrap"
grunt configAndroidBootstrap
grunt buildAndroidBootstrap
@@ -77,6 +85,16 @@ reset_android() {
grunt buildAndroidApp:ApiDemos
}
reset_selendroid() {
echo "---- RESETTING SELENDROID ----"
get_apidemos
echo "Downloading/updating selendroid"
rm -rf submodules/selendroid/selendroid-server/target
git submodule update --init submodules/selendroid
rm -rf selendroid
ln -s $appium_home/submodules/selendroid $appium_home/selendroid
}
cleanup() {
echo "---- CLEANING UP ----"
echo "Cleaning temp files"
@@ -92,4 +110,7 @@ fi
if [ $should_reset_android -eq 1 ]; then
reset_android
fi
if [ $should_reset_selendroid -eq 1 ]; then
reset_selendroid
fi
cleanup
+34
View File
@@ -0,0 +1,34 @@
/*global it:true */
"use strict";
var path = require('path')
, appPath = path.resolve(__dirname, "../../../sample-code/apps/ApiDemos/bin/ApiDemos-debug.apk")
, appPkg = "com.example.android.apis"
, appAct = "ApiDemos"
, driverBlock = require("../../helpers/driverblock.js")
, describeWd = driverBlock.describeForApp(appPath, "selendroid", appPkg, appAct)
, should = require('should');
describeWd('basic', function(h) {
it('should find and click an element', function(done) {
// selendroid appears to have some issues with implicit waits
// hence the timeouts
setTimeout(function() {
h.driver.elementByName('App', function(err, el) {
should.not.exist(err);
should.exist(el);
el.click(function(err) {
should.not.exist(err);
setTimeout(function() {
h.driver.elementByLinkText("Action Bar", function(err, el) {
should.not.exist(err);
should.exist(el);
done();
});
}, 1000);
});
});
}, 1000);
});
});
+6 -4
View File
@@ -108,22 +108,24 @@ var describeForApp = function(app, device, appPackage, appActivity) {
if (typeof device === "undefined") {
device = "ios";
}
var browserName, appPath;
var browserName, appPath, realDevice;
if (device === "ios") {
realDevice = "iPhone Simulator";
browserName = "iOS";
} else if (device === "android") {
browserName = "Android";
browserName = realDevice = "Android";
} else if (device === "selendroid") {
browserName = realDevice = "Selendroid";
}
if (/\//.exec(app) || /\./.exec(app)) {
appPath = app;
} else {
if (device === "ios") {
appPath = path.resolve(__dirname, "../../sample-code/apps/" + app + "/build/Release-iphonesimulator/" + app + ".app");
} else if (device === "android") {
} else if (device === "android" || device === "selendroid") {
appPath = path.resolve(__dirname, "../../sample-code/apps/" + app + "/bin/" + app + "-debug.apk");
}
}
var realDevice = device == "ios" ? "iPhone Simulator" : "Android";
return function(desc, tests, host, port, caps, extraCaps) {
if (typeof extraCaps === "undefined") {
+386 -276
View File
@@ -11,6 +11,7 @@ var spawn = require('child_process').spawn
, testZipArchive = require('../app/helpers').testZipArchive
, async = require('async')
, ncp = require('ncp')
, mkdirp = require('mkdirp')
, _ = require('underscore');
var noop = function() {};
@@ -27,7 +28,8 @@ var ADB = function(opts, android) {
// Don't uninstall if using fast reset.
// Uninstall if reset is set and fast reset isn't.
this.skipUninstall = opts.fastReset || !(opts.reset || false);
this.port = opts.port || 4724;
this.systemPort = opts.port || 4724;
this.devicePort = opts.devicePort || 4724;
this.avdName = opts.avdName;
this.appPackage = opts.appPackage;
this.appActivity = opts.appActivity;
@@ -45,6 +47,7 @@ var ADB = function(opts, android) {
this.portForwarded = false;
this.debugMode = true;
this.fastReset = opts.fastReset;
this.cleanApp = opts.cleanApp || this.fastReset;
this.cleanAPK = '/tmp/' + this.appPackage + '.clean.apk';
// This is set to true when the bootstrap jar crashes.
this.restartBootstrap = false;
@@ -54,193 +57,192 @@ var ADB = function(opts, android) {
this.cmdCb = null;
};
ADB.prototype.checkAdbPresent = function(cb) {
ADB.prototype.checkSdkBinaryPresent = function(binary, cb) {
var binaryLoc = null;
if (this.sdkRoot) {
this.adb = path.resolve(this.sdkRoot, "platform-tools", "adb");
this.debug("Using adb from " + this.adb);
cb(null, this);
binaryLoc = path.resolve(this.sdkRoot, "platform-tools", binary);
this.debug("Using " + binary + " from " + binaryLoc);
cb(null, binaryLoc);
} else {
exec("which adb", _.bind(function(err, stdout) {
exec("which " + binary, _.bind(function(err, stdout) {
if (stdout) {
this.debug("Using adb from " + stdout);
this.adb = stdout;
cb(null, this);
this.debug("Using " + binary + " from " + stdout);
cb(null, stdout);
} else {
cb("Could not find adb; do you have android SDK installed?", null);
cb(new Error("Could not find " + binary + "; do you have android " +
"SDK installed?"),
null);
}
}, this));
}
};
ADB.prototype.checkAdbPresent = function(cb) {
this.checkSdkBinaryPresent("adb", _.bind(function(err, binaryLoc) {
if (err) return cb(err);
this.adb = binaryLoc;
cb(null);
}, this));
};
ADB.prototype.retargetManifest = function(newPkg, newTarget, inFile, outFile, cb) {
var data = fs.readFileSync(inFile, "utf8");
var rePkg = /package="([^"]+)"/;
var reTarget = /targetPackage="([^"]+)"/;
var matchPkg = rePkg.exec(data);
var matchTarget = reTarget.exec(data);
if (newPkg && !matchPkg) {
logger.debug("Could not find package= in manifest");
return cb("could not find package= in manifest");
}
if (newTarget && !matchTarget) {
logger.debug("Could not find targetPackage= in manifest");
return cb("could not find targetPackage= in manifest");
}
if (newPkg) {
var newPkgData = matchPkg[0].replace(matchPkg[1], newPkg);
data = data.replace(matchPkg[0], newPkgData);
}
if (newTarget) {
var newTargetData = matchTarget[0].replace(matchTarget[1], newTarget);
data = data.replace(matchTarget[0], newTargetData);
}
fs.writeFileSync(outFile, data, "utf8");
logger.debug("Created manifest");
cb(null);
};
// Fast reset
ADB.prototype.buildFastReset = function(skipAppSign, cb) {
// Create manifest
var me = this;
var targetAPK = me.apkPath;
var targetPackage = me.appPackage;
// gsub AndroidManifest.xml using targetPackage
var manifestPath = path.resolve(__dirname, '../app/android/AndroidManifest.xml.src');
var cleanAPKSrc = path.resolve(__dirname, '../app/android/Clean.apk');
var newPackage = me.appPackage + '.clean';
var inFile = path.resolve(__dirname, '../app/android/AndroidManifest.xml.src');
var outFile = inFile.substr(0, inFile.length - '.src'.length);
var utf8 = 'utf8';
var data = fs.readFileSync(manifestPath, utf8);
var manifest = manifestPath.substr(0, manifestPath.length - '.src'.length);
var resignApks = function(cb) {
// Resign clean apk and target apk
var apks = [ me.cleanAPK ];
if (!skipAppSign) {
logger.debug("Signing app and clean apk.");
apks.push(targetAPK);
} else {
logger.debug("Skip app sign. Sign clean apk.");
}
me.sign(apks, cb);
};
var rePkg = /package="([^"]+)"/;
var reTarget = /targetPackage="([^"]+)"/;
var matchPkg = rePkg.exec(data);
var matchTarget = reTarget.exec(data);
if (!matchPkg) {
logger.debug("Could not find package= in manifest");
return cb("could not find package= in manifest");
}
if (!matchTarget) {
logger.debug("Could not find targetPackage= in manifest");
return cb("could not find targetPackage= in manifest");
}
var newPkg = matchPkg[0].replace(matchPkg[1], targetPackage + '.clean');
var newTarget = matchTarget[0].replace(matchTarget[1], targetPackage);
var newContent = data.replace(matchPkg[0], newPkg);
newContent = newContent.replace(matchTarget[0], newTarget);
fs.writeFileSync(manifest, newContent, utf8);
logger.debug("Created manifest");
async.series(
[
function(cb) {
me.compileManifest(function(err) { if (err) return cb(err); cb(null); }, manifest);
},
function(cb) {
me.insertManifest(manifest, skipAppSign, function(err) {
if (err) return cb(err);
cb(null);
});
},
],
// Invoke top level function cb
function(err) { cb(err); }
);
async.series([
function(cb) { me.retargetManifest(newPackage, me.appPackage, inFile, outFile, cb); },
function(cb) { me.checkSdkBinaryPresent("aapt", cb); },
function(cb) { me.compileManifest(outFile, cb); },
function(cb) { me.insertManifest(outFile, cleanAPKSrc, me.cleanAPK, cb); },
function(cb) { resignApks(cb); }
], cb);
};
ADB.prototype.compileManifest = function(cb, manifest) {
async.series(
[
function(cb) {
exec('which aapt', function(err, stdout) {
if (!stdout) {
return cb(new Error("Error finding aapt binary, is it on your path?"));
}
cb(null);
});
},
function(cb) {
var androidHome = process.env.ANDROID_HOME;
ADB.prototype.insertSelendroidManifest = function(serverPath, cb) {
var me = this
, newServerPath = me.selendroidServerPath
, srcManifest = path.resolve(__dirname, "../selendroid/selendroid-gem",
"selendroid-prebuild/AndroidManifest.xml")
, dstDir = '/tmp/' + this.appPackage
, dstManifest = dstDir + '/AndroidManifest.xml';
var platforms = androidHome + '/platforms/';
var platform = 'android-17';
// android-17 may be called android-4.2
if (!fs.existsSync(platforms + platform)) {
platform = 'android-4.2';
if (!fs.existsSync(platforms + platform)) {
return cb("Platform doesn't exist " + platform);
}
}
// Compile manifest into manifest.xml.apk
var compile_manifest = ['aapt package -M "', manifest,
'" -I "', platforms + platform + '/android.jar" -F "',
manifest, '.apk" -f'].join('');
logger.debug(compile_manifest);
exec(compile_manifest, {}, function(err, stdout, stderr) {
if (err) {
logger.debug(stderr);
return cb("error compiling manifest");
}
logger.debug("Compiled manifest");
cb(null);
});
},
],
// Invoke top level function cb
function(err) { cb(err); }
);
async.series([
function(cb) { mkdirp(dstDir, cb); },
function(cb) { me.retargetManifest(null, me.appPackage, srcManifest,
dstManifest, cb); },
function(cb) { me.checkSdkBinaryPresent("aapt", cb); },
function(cb) { me.compileManifest(dstManifest, cb); },
function(cb) { me.insertManifest(dstManifest, serverPath, newServerPath,
cb); }
], cb);
};
ADB.prototype.insertManifest = function(manifest, skipAppSign, cb) {
var me = this;
var targetAPK = me.apkPath;
var cleanAPK = me.cleanAPK;
async.series(
[
// Extract compiled manifest from manifest.xml.apk
function(cb) {
unzipFile(manifest + '.apk', function(err, stderr) {
if (err) {
logger.debug(stderr);
return cb(err);
}
cb(null);
});
},
// Insert compiled manfiest into /tmp/appPackage.clean.apk
function(cb) {
var cleanAPKSource = path.resolve(__dirname, '../app/android/Clean.apk');
logger.debug("Writing tmp apk. " + cleanAPKSource + ' to ' + cleanAPK);
ADB.prototype.compileManifest = function(manifest, cb) {
var androidHome = process.env.ANDROID_HOME
, platforms = androidHome + '/platforms/'
, platform = 'android-17';
// Write clean apk to /tmp
ncp(cleanAPKSource, cleanAPK, function(err) {
if (err) return cb(err);
cb(null);
});
},
function(cb) {
logger.debug("Testing tmp clean apk.");
testZipArchive(cleanAPK, function(err) {
if (err) return cb(err);
cb(null);
});
},
function(cb) {
// -j = keep only the file, not the dirs
// -m = move manifest into target apk.
var replaceCmd = 'zip -j -m "' + cleanAPK + '" "' + manifest + '"';
logger.debug(replaceCmd);
exec(replaceCmd, {}, function(err) {
if (err) {
return cb(err);
}
logger.debug("Inserted manifest.");
cb(null);
});
},
// Resign clean apk and target apk
function(cb) {
var apks = [ cleanAPK ];
if (!skipAppSign) {
logger.debug("Signing app and clean apk.");
apks.push(targetAPK);
} else {
logger.debug("Skip app sign. Sign clean apk.");
}
me.sign(function(err) {
if (err) return cb(err);
cb(null);
}, apks);
// android-17 may be called android-4.2
if (!fs.existsSync(platforms + platform)) {
platform = 'android-4.2';
if (!fs.existsSync(platforms + platform)) {
return cb(new Error("Platform doesn't exist " + platform));
}
}
// Compile manifest into manifest.xml.apk
var compileManifest = ['aapt package -M "', manifest,
'" -I "', platforms + platform + '/android.jar" -F "',
manifest, '.apk" -f'].join('');
logger.debug(compileManifest);
exec(compileManifest, {}, function(err, stdout, stderr) {
if (err) {
logger.debug(stderr);
return cb("error compiling manifest");
}
logger.debug("Compiled manifest");
cb(null);
});
};
ADB.prototype.insertManifest = function(manifest, srcApk, dstApk, cb) {
var extractManifest = function(cb) {
// Extract compiled manifest from manifest.xml.apk
unzipFile(manifest + '.apk', function(err, stderr) {
if (err) {
logger.debug(stderr);
return cb(err);
}
],
// Invoke top level function cb
function(err) { cb(err); }
);
cb(null);
});
};
var createTmpApk = function(cb) {
logger.debug("Writing tmp apk. " + srcApk + ' to ' + dstApk);
ncp(srcApk, dstApk, cb);
};
var testDstApk = function(cb) {
logger.debug("Testing new tmp apk.");
testZipArchive(dstApk, cb);
};
var moveManifest = function(cb) {
// Insert compiled manfiest into /tmp/appPackage.clean.apk
// -j = keep only the file, not the dirs
// -m = move manifest into target apk.
var replaceCmd = 'zip -j -m "' + dstApk + '" "' + manifest + '"';
logger.debug(replaceCmd);
exec(replaceCmd, {}, function(err) {
if (err) {
return cb(err);
}
logger.debug("Inserted manifest.");
cb(null);
});
};
async.series([
function(cb) { extractManifest(cb); },
function(cb) { createTmpApk(cb); },
function(cb) { testDstApk(cb); },
function(cb) { moveManifest(cb); }
], cb);
};
// apks is an array of strings.
ADB.prototype.sign = function(cb, apks) {
ADB.prototype.sign = function(apks, cb) {
var signPath = path.resolve(__dirname, '../app/android/sign.jar');
var resign = 'java -jar "' + signPath + '" "' + apks.join('" "') + '" --override';
logger.debug("Resigning: " + resign);
@@ -250,10 +252,8 @@ ADB.prototype.sign = function(cb, apks) {
return cb(new Error("Could not sign one or more apks. Are you sure " +
"the file paths are correct: " +
JSON.stringify(apks)));
} else if (err) {
return cb(err);
}
cb(null);
cb(err);
});
};
@@ -278,6 +278,8 @@ ADB.prototype.checkFastReset = function(cb) {
return cb(null);
}
if (!this.appPackage) return cb(new Error("appPackage must be set."));
var me = this;
me.checkApkCert(me.cleanAPK, function(cleanSigned){
me.checkApkCert(me.apkPath, function(appSigned){
@@ -287,7 +289,7 @@ ADB.prototype.checkFastReset = function(cb) {
} else {
if (!appSigned) {
// Resign app apk because it's not signed.
me.sign(function(err) { if (err) return cb(err); cb(null); }, [me.apkPath]);
me.sign([me.apkPath], cb);
} else {
// App and clean are already existing and signed.
cb(null);
@@ -297,66 +299,163 @@ ADB.prototype.checkFastReset = function(cb) {
});
};
ADB.prototype.start = function(onReady, onExit) {
ADB.prototype.getDeviceWithRetry = function(cb) {
var me = this;
var getDevices = function(innerCb) {
me.getConnectedDevices(function(err, devices) {
if (devices.length === 0 || err) {
return innerCb(new Error("Could not find a connected Android device."));
}
innerCb(null);
});
};
getDevices(function(err) {
if (err) {
logger.info("Could not find devices, restarting adb server...");
me.restartAdb(function() {
getDevices(cb);
});
} else {
cb(null);
}
});
};
ADB.prototype.prepareDevice = function(onReady) {
var me = this;
async.series([
function(cb) { me.checkAdbPresent(cb); },
function(cb) { me.getDeviceWithRetry(cb);},
function(cb) { me.waitForDevice(cb); },
function(cb) { me.checkFastReset(cb); }
], onReady);
};
ADB.prototype.startAppium = function(onReady, onExit) {
var me = this
, doRun = function(err) {
if (err) return onReady(err);
me.runBootstrap(onReady, onExit);
};
this.onExit = onExit;
var doRun = _.bind(function() {
this.runBootstrap(onReady, onExit);
}, this);
logger.debug("Using fast reset? " + this.fastReset);
var me = this;
async.series(
[
function(cb) {
me.checkAdbPresent(function(err) { if (err) return onReady(err); cb(null); });
},
function(cb) {
var getDevices = function(innerCb) {
me.getConnectedDevices(function(err, devices) {
if (devices.length === 0 || err) {
innerCb("Could not find a connected Android device.");
}
innerCb(null);
});
};
getDevices(function(err) {
if (err) {
logger.info("restarting...");
me.restartAdb(function() {
getDevices(cb);
});
} else {
cb(null);
}
});
},
function(cb) {
me.waitForDevice(function(err) { if (err) return onReady(err); cb(null); });
},
function(cb) {
me.pushAppium(function(err) { if (err) return onReady(err); cb(null); });
},
function(cb) {
me.forwardPort(function(err) { if (err) return onReady(err); cb(null); });
},
function(cb) {
if (!me.appPackage) return onReady("appPackage must be set.");
me.checkFastReset(function(err) {
if (err) return onReady(err); cb(null);
});
},
function(cb) {
me.installApp(function(err) { if (err) return onReady(err); cb(null); });
},
function(cb) {
me.startApp(function(err) {
if (err) return onReady(err);
doRun(function(){ cb(null); });
});
async.series([
function(cb) { me.prepareDevice(cb); },
function(cb) { me.installApp(cb); },
function(cb) { me.forwardPort(cb); },
function(cb) { me.pushAppium(cb); },
function(cb) { me.startApp(cb); }
], doRun);
};
ADB.prototype.startSelendroid = function(serverPath, onReady) {
var me = this
, modServerExists = false;
this.selendroidServerPath = serverPath.replace(/\.apk$/, ".mod.apk");
var checkModServerExists = function(cb) {
fs.stat(me.selendroidServerPath, function(err) {
modServerExists = !err;
cb();
});
};
var conditionalUninstallSelendroid = function(cb) {
if (!modServerExists) {
// assume we're going to rebuild selendroid and therefore
// need to uninstall it if it's already on device
logger.info("Rebuilt selendroid apk does not exist, uninstalling " +
"any instances of it on device to make way for new one");
me.uninstallApk('org.openqa.selendroid', cb);
} else {
logger.info("Rebuilt selendroid apk exists, doing nothing");
cb();
}
};
var conditionalInsertManifest = function(cb) {
if (!modServerExists) {
logger.info("Rebuilt selendroid server does not exist, inserting " +
"modified manifest");
me.insertSelendroidManifest(serverPath, cb);
} else {
logger.info("Rebuilt selendroid server already exists, no need to " +
"rebuild it with a new manifest");
cb();
}
};
var conditionalInstallSelendroid = function(cb) {
me.checkAppInstallStatus('org.openqa.selendroid', function(e, installed) {
if (!installed) {
logger.info("Rebuilt selendroid is not installed, installing it");
me.installApk(me.selendroidServerPath, cb);
} else {
logger.info("Rebuilt selendroid is already installed");
cb();
}
]
);
});
};
async.series([
function(cb) { me.prepareDevice(cb); },
function(cb) { checkModServerExists(cb); },
function(cb) { conditionalUninstallSelendroid(cb); },
function(cb) { conditionalInsertManifest(cb); },
function(cb) { me.checkSelendroidCerts(me.selendroidServerPath, cb); },
function(cb) { conditionalInstallSelendroid(cb); },
function(cb) { me.installApp(cb); },
function(cb) { me.forwardPort(cb); },
function(cb) { me.pushSelendroid(cb); },
function(cb) { logger.info("Selendroid server is launching"); cb(); }
], onReady);
};
ADB.prototype.pushSelendroid = function(cb) {
var cmd = "adb shell am instrument -e main_activity '" + this.appPackage +
"." + this.appActivity + "' org.openqa.selendroid/" +
"org.openqa.selendroid.ServerInstrumentation";
logger.info("Starting instrumentation process for selendroid with cmd: " +
cmd);
exec(cmd, function(err, stdout) {
if (stdout.indexOf("Exception") !== -1) {
logger.error(stdout);
var msg = stdout.split("\n")[0] || "Unknown exception starting selendroid";
return cb(new Error(msg));
}
cb();
});
};
ADB.prototype.checkSelendroidCerts = function(serverPath, cb) {
var me = this
, alreadyReturned = false
, checks = 0;
var onDoneSigning = function() {
checks++;
if (checks === 2 && !alreadyReturned) {
cb();
}
};
// these run in parallel
var apks = [serverPath, this.apkPath];
_.each(apks, function(apk) {
logger.info("Checking signed status of " + apk);
me.checkApkCert(apk, function(isSigned) {
if (isSigned) return onDoneSigning();
me.sign([apk], function(err) {
if (err && !alreadyReturned) {
alreadyReturned = true;
return cb(err);
}
onDoneSigning();
});
});
});
};
ADB.prototype.getConnectedDevices = function(cb) {
@@ -383,9 +482,9 @@ ADB.prototype.getConnectedDevices = function(cb) {
ADB.prototype.forwardPort = function(cb) {
this.requireDeviceId();
var devicePort = 4724;
this.debug("Forwarding system:" + this.port + " to device:" + devicePort);
var arg = "tcp:" + this.port + " tcp:" + devicePort;
this.debug("Forwarding system:" + this.systemPort + " to device:" +
this.devicePort);
var arg = "tcp:" + this.systemPort + " tcp:" + this.devicePort;
exec(this.adbCmd + " forward " + arg, _.bind(function(err) {
if (err) {
logger.error(err);
@@ -445,7 +544,7 @@ ADB.prototype.checkForSocketReady = function(output) {
if (/Appium Socket Server Ready/.exec(output)) {
this.requirePortForwarded();
this.debug("Connecting to server on device...");
this.socketClient = net.connect(this.port, _.bind(function() {
this.socketClient = net.connect(this.systemPort, _.bind(function() {
this.debug("Connected!");
this.onSocketReady(null);
}, this));
@@ -657,13 +756,13 @@ ADB.prototype.waitForDevice = function(cb) {
ADB.prototype.restartAdb = function(cb) {
logger.info("Killing ADB server so it will come back online");
var cmd = this.adb + " kill-server";
exec(cmd, _.bind(function(err) {
exec(cmd, function(err) {
if (err) {
logger.error("Error killing ADB server, going to see if it's online " +
"anyway");
}
cb();
}, this));
});
};
ADB.prototype.pushAppium = function(cb) {
@@ -850,58 +949,69 @@ ADB.prototype.runFastReset = function(cb) {
});
};
ADB.prototype.checkAppInstallStatus = function(pkg, cb) {
var installed = false
, cleanInstalled = false;
this.requireDeviceId();
logger.debug("Getting install/clean status for " + pkg);
var listPkgCmd = this.adbCmd + " shell pm list packages -3 " + pkg;
console.log(listPkgCmd);
exec(listPkgCmd, function(err, stdout) {
//console.log("Packages found:");
//console.log(stdout);
var apkInstalledRgx = new RegExp('^package:' +
pkg.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
installed = !!apkInstalledRgx.test(stdout);
var cleanInstalledRgx = new RegExp('^package:' +
(pkg + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
cleanInstalled = !!cleanInstalledRgx.test(stdout);
cb(null, installed, cleanInstalled);
});
};
ADB.prototype.installApp = function(cb) {
var me = this;
var next = function() {
me.requireDeviceId();
me.requireApk();
var installApp = false;
var installClean = false;
async.series(
[
function(cb) {
var listPkgCmd = me.adbCmd + " shell pm list packages -3 " + me.appPackage;
exec(listPkgCmd, function(err, stdout) {
var apkInstalledRgx = new RegExp('^package:' + me.appPackage.replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
installApp = ! apkInstalledRgx.test(stdout);
var cleanInstalledRgx = new RegExp('^package:' + (me.appPackage + '.clean').replace(/([^a-zA-Z])/g, "\\$1") + '$', 'm');
installClean = ! cleanInstalledRgx.test(stdout);
me.debug("Packages found:");
me.debug(stdout);
cb(null);
});
},
function(cb) {
if (installApp) {
me.debug("Installing app apk");
me.installApk(me.apkPath, function(err) {
if (err) return cb(err);
return cb(null);
});
} else { cb(null); }
},
function(cb) {
if (installClean) {
me.debug("Installing clean apk");
me.installApk(me.cleanAPK, function(err) { if (err) return cb(err); return cb(null); });
} else { cb(null); }
},
function(cb) {
// App is already installed so reset it.
if (!installApp) {
me.runFastReset(function(err) {
if (err) return cb(err);
return cb(null);
});
} else { cb(null); }
}
],
// Top level callback.
function(err) { cb(err); }
);
var me = this
, installApp = false
, installClean = false;
me.requireDeviceId();
me.requireApk();
var determineInstallAndCleanStatus = function(cb) {
me.checkAppInstallStatus(me.appPackage, function(err, installed, cleaned) {
installApp = !installed;
installClean = !cleaned;
cb();
});
};
next();
var doInstall = function(cb) {
if (installApp) {
me.debug("Installing app apk");
me.installApk(me.apkPath, cb);
} else { cb(null); }
};
var doClean = function(cb) {
if (installClean && me.cleanApp) {
me.debug("Installing clean apk");
me.installApk(me.cleanAPK, cb);
} else { cb(null); }
};
var doFastReset = function(cb) {
// App is already installed so reset it.
if (!installApp && me.fastReset) {
me.runFastReset(cb);
} else { cb(null); }
};
async.series([
function(cb) { determineInstallAndCleanStatus(cb); },
function(cb) { doInstall(cb); },
function(cb) { doClean(cb); },
function(cb) { doFastReset(cb); }
], cb);
};
ADB.prototype.back = function(cb) {