mirror of
https://github.com/appium/appium.git
synced 2026-02-15 22:40:10 -06:00
276 lines
9.9 KiB
JavaScript
276 lines
9.9 KiB
JavaScript
"use strict";
|
|
|
|
var logger = require('../../server/logger.js').get('appium')
|
|
, _ = require('underscore')
|
|
, errors = require('../../server/errors.js')
|
|
, UnknownError = errors.UnknownError
|
|
, async = require('async')
|
|
, Chromedriver = require('appium-chromedriver')
|
|
, status = require("../../server/status.js");
|
|
|
|
var androidHybrid = {};
|
|
|
|
androidHybrid.chromedriver = null;
|
|
androidHybrid.sessionChromedrivers = {};
|
|
|
|
androidHybrid.listWebviews = function (cb) {
|
|
logger.debug("Getting a list of available webviews");
|
|
var webviews = [];
|
|
var definedDeviceSocket = this.args.androidDeviceSocket;
|
|
this.adb.shell("cat /proc/net/unix", function (err, out) {
|
|
if (err) return cb(err);
|
|
_.each(out.split("\n"), function (line) {
|
|
line = line.trim();
|
|
var webviewPid = line.match(/@?webview_devtools_remote_(\d+)/);
|
|
if (definedDeviceSocket) {
|
|
if (line.indexOf("@" + definedDeviceSocket) ===
|
|
line.length - definedDeviceSocket.length - 1) {
|
|
if (webviewPid) {
|
|
webviews.push(this.WEBVIEW_BASE + webviewPid[1]);
|
|
} else {
|
|
webviews.push(this.CHROMIUM_WIN);
|
|
}
|
|
}
|
|
} else if (webviewPid) {
|
|
// for multiple webviews a list of 'WEBVIEW_<index>' will be returned
|
|
// where <index> is zero based (same is in selendroid)
|
|
webviews.push(this.WEBVIEW_BASE + webviewPid[1]);
|
|
}
|
|
}.bind(this));
|
|
webviews = _.uniq(webviews);
|
|
|
|
if (definedDeviceSocket) {
|
|
return cb(null, webviews);
|
|
}
|
|
|
|
var webviewsTmp = webviews;
|
|
webviews = [];
|
|
|
|
var getProcessNameFromWebview = function (view, cb) {
|
|
this.getProcessNameFromWebview(view, function (err, pkg) {
|
|
if (err) return cb(err);
|
|
webviews.push(this.WEBVIEW_BASE + pkg);
|
|
cb();
|
|
}.bind(this));
|
|
}.bind(this);
|
|
|
|
async.each(webviewsTmp, getProcessNameFromWebview, function (err) {
|
|
if (err) return cb(err);
|
|
logger.debug("Available contexts: " + this.contexts);
|
|
logger.debug(JSON.stringify(webviews));
|
|
cb(null, webviews);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
};
|
|
|
|
var previousState = {};
|
|
|
|
// remember whether we were previously proxying to a chromedriver or not
|
|
androidHybrid.rememberProxyState = function () {
|
|
previousState.isProxy = this.isProxy;
|
|
};
|
|
|
|
androidHybrid.restoreProxyState = function () {
|
|
this.isProxy = previousState.isProxy;
|
|
};
|
|
|
|
androidHybrid.getProcessNameFromWebview = function (webview, cb) {
|
|
// webview_devtools_remote_4296 => 4296
|
|
var pid = webview.match(/\d+$/);
|
|
if (!pid) return cb("No pid for webview " + webview);
|
|
pid = pid[0];
|
|
logger.debug(webview + " mapped to pid " + pid);
|
|
|
|
logger.debug("Getting process name for webview");
|
|
this.adb.shell("ps", function (err, out) {
|
|
if (err) return cb(err);
|
|
var pkg = "unknown";
|
|
|
|
var lines = out.split(/\r?\n/);
|
|
/*
|
|
USER PID PPID VSIZE RSS WCHAN PC NAME
|
|
u0_a136 6248 179 946000 48144 ffffffff 4005903e R com.example.test
|
|
*/
|
|
var header = lines[0].trim().split(/\s+/);
|
|
// the column order may not be identical on all androids
|
|
// dynamically locate the pid and name column.
|
|
var pidColumn = header.indexOf("PID");
|
|
var pkgColumn = header.indexOf("NAME") + 1;
|
|
|
|
_.find(lines, function (line) {
|
|
line = line.trim().split(/\s+/);
|
|
if (line[pidColumn].indexOf(pid) !== -1) {
|
|
logger.debug("Parsed pid: " + line[pidColumn] + " pkg: " + line[pkgColumn]);
|
|
logger.debug("from: " + line);
|
|
pkg = line[pkgColumn];
|
|
return pkg; // exit from _.find
|
|
}
|
|
});
|
|
|
|
logger.debug("returning process name: " + pkg);
|
|
cb(null, pkg);
|
|
});
|
|
};
|
|
|
|
androidHybrid.startChromedriverProxy = function (context, cb) {
|
|
cb = _.once(cb);
|
|
logger.debug("Connecting to chrome-backed webview");
|
|
if (this.chromedriver !== null) {
|
|
return cb(new Error("We already have a chromedriver instance running"));
|
|
}
|
|
|
|
if (this.sessionChromedrivers[context]) {
|
|
// in the case where we've already set up a chromedriver for a context,
|
|
// we want to reconnect to it, not create a whole new one
|
|
this.setupExistingChromedriver(context, cb);
|
|
} else {
|
|
this.setupNewChromedriver(context, cb);
|
|
}
|
|
};
|
|
|
|
androidHybrid.setupNewChromedriver = function (context, cb) {
|
|
var chromeArgs = {
|
|
port: this.args.chromeDriverPort,
|
|
executable: this.args.chromedriverExecutable
|
|
};
|
|
this.chromedriver = new Chromedriver(chromeArgs);
|
|
this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver);
|
|
this.rememberProxyState();
|
|
this.isProxy = true;
|
|
var caps = {
|
|
chromeOptions: {
|
|
androidPackage: this.args.appPackage,
|
|
androidUseRunningApp: true
|
|
}
|
|
};
|
|
if (this.args.enablePerformanceLogging) {
|
|
caps.loggingPrefs = {performance: 'ALL'};
|
|
}
|
|
// For now the only known arg passed this way is androidDeviceSocket used
|
|
// by Operadriver (deriving from Chromedriver) // We don't know how other
|
|
// Chromium embedders will call this argument so for now it's name needs to
|
|
// be configurable. When Google adds the androidDeviceSocket argument to
|
|
// the original Chromedriver then we will be sure about its name for all
|
|
// Chromium embedders (as their Webdrivers will derive from Chromedriver)
|
|
if (this.args.specialChromedriverSessionArgs) {
|
|
_.each(this.args.specialChromedriverSessionArgs, function (val, option) {
|
|
logger.debug("This method is being deprecated. Apply chromeOptions " +
|
|
"normally to pass along options,see sites.google.com/a/" +
|
|
"chromium.org/chromedriver/capabilities for more info");
|
|
caps.chromeOptions[option] = val;
|
|
});
|
|
}
|
|
caps = this.decorateChromeOptions(caps);
|
|
this.chromedriver.on(Chromedriver.EVENT_CHANGED, function (msg) {
|
|
if (msg.state === Chromedriver.STATE_STOPPED) {
|
|
// bind our stop/exit handler, passing in context so we know which
|
|
// one stopped unexpectedly
|
|
this.onChromedriverStop(context);
|
|
}
|
|
}.bind(this));
|
|
this.chromedriver.start(caps).nodeify(function (err) {
|
|
if (err) return cb(err);
|
|
// save the chromedriver object under the context
|
|
this.sessionChromedrivers[context] = this.chromedriver;
|
|
cb();
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
androidHybrid.setupExistingChromedriver = function (context, cb) {
|
|
logger.debug("Found existing Chromedriver for context '" + context + "'." +
|
|
" Using it.");
|
|
this.rememberProxyState();
|
|
this.chromedriver = this.sessionChromedrivers[context];
|
|
this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver);
|
|
this.isProxy = true;
|
|
|
|
// check the status by sending a simple window-based command to ChromeDriver
|
|
// if there is an error, we want to recreate the ChromeDriver session
|
|
this.chromedriver.hasWorkingWebview().nodeify(function (err, works) {
|
|
if (err) return cb(err);
|
|
if (works) return cb();
|
|
logger.debug("ChromeDriver is not associated with a window. " +
|
|
"Re-initializing the session.");
|
|
this.chromedriverRestartingContext = context;
|
|
this.chromedriver.restart().nodeify(function (err) {
|
|
if (err) return cb(err);
|
|
this.chromedriverRestartingContext = null;
|
|
cb();
|
|
}.bind(this));
|
|
}.bind(this));
|
|
};
|
|
|
|
androidHybrid.onChromedriverStop = function (context) {
|
|
logger.warn("Chromedriver for context " + context + " stopped unexpectedly");
|
|
if (context === this.curContext) {
|
|
// if we don't have a stop callback, we exited unexpectedly and so want
|
|
// to shut down the session and respond with an error
|
|
// TODO: this kind of thing should be emitted and handled by a higher-level
|
|
// controlling function
|
|
var error = new UnknownError("Chromedriver quit unexpectedly during session");
|
|
logger.error(error.message);
|
|
if (typeof this.cbForCurrentCmd === "function") {
|
|
this.shutdown(function () {
|
|
this.cbForCurrentCmd(error, null);
|
|
}.bind(this));
|
|
}
|
|
} else if (context !== this.chromedriverRestartingContext) {
|
|
// if a Chromedriver in the non-active context barfs, we don't really
|
|
// care, we'll just make a new one next time we need the context.
|
|
// The only time we ignore this is if we know we're in the middle of a
|
|
// Chromedriver restart
|
|
logger.warn("Chromedriver quit unexpectedly, but it wasn't the active " +
|
|
"context, ignoring");
|
|
delete this.sessionChromedrivers[context];
|
|
}
|
|
};
|
|
|
|
androidHybrid.suspendChromedriverProxy = function (cb) {
|
|
this.chromedriver = null;
|
|
this.restoreProxyState();
|
|
cb();
|
|
};
|
|
|
|
androidHybrid.stopChromedriverProxies = function (ocb) {
|
|
async.eachSeries(Object.keys(this.sessionChromedrivers), function (context, cb) {
|
|
logger.debug("Stopping chromedriver for context " + context);
|
|
// stop listening for the stopped state event
|
|
this.sessionChromedrivers[context].removeAllListeners(Chromedriver.EVENT_CHANGED);
|
|
this.sessionChromedrivers[context].stop().nodeify(function (err) {
|
|
if (err) logger.warn("Error stopping Chromedriver: " + err.message);
|
|
// chromedriver isn't valid anymore, so remove it from context list
|
|
delete this.sessionChromedrivers[context];
|
|
cb();
|
|
}.bind(this));
|
|
}.bind(this), function (err) {
|
|
// if one of these fails, go back to last proxy state and error out
|
|
this.restoreProxyState();
|
|
ocb(err);
|
|
}.bind(this));
|
|
};
|
|
|
|
androidHybrid.defaultWebviewName = function () {
|
|
return this.WEBVIEW_BASE + this.appProcess;
|
|
};
|
|
|
|
androidHybrid.initAutoWebview = function (cb) {
|
|
if (this.args.autoWebview) {
|
|
logger.debug('Setting auto webview');
|
|
var viewName = this.defaultWebviewName();
|
|
var timeout = (this.args.autoWebviewTimeout) || 2000;
|
|
this.setContext(viewName, function (err, res) {
|
|
if (err && res.status !== status.codes.NoSuchContext.code) return cb(err);
|
|
if (res.status === status.codes.Success.code) return cb();
|
|
setTimeout(function () {
|
|
logger.debug("Retrying context switch with timeout '" + timeout + "'");
|
|
this.setContext(viewName, cb);
|
|
}.bind(this), timeout);
|
|
}.bind(this));
|
|
} else {
|
|
cb();
|
|
}
|
|
};
|
|
|
|
module.exports = androidHybrid;
|