diff --git a/android/adb.js b/android/adb.js index ac46eb681..e1ed18b76 100644 --- a/android/adb.js +++ b/android/adb.js @@ -29,6 +29,7 @@ var ADB = function(opts, android) { } this.sdkRoot = opts.sdkRoot; this.udid = opts.udid; + this.webSocket = opts.webSocket; // Don't uninstall if using fast reset. // Uninstall if reset is set and fast reset isn't. this.skipUninstall = opts.fastReset || !(opts.reset || false); @@ -719,7 +720,6 @@ ADB.prototype.runBootstrap = function(readyCb, exitCb) { }; ADB.prototype.checkForSocketReady = function(output) { - logger.info("Checking for appium socket server to be ready"); if (/Appium Socket Server Ready/.test(output)) { this.requirePortForwarded(); this.debug("Connecting to server on device..."); @@ -811,6 +811,12 @@ ADB.prototype.handleBootstrapOutput = function(output) { match = re.exec(line); if (match) { logger.info("[ANDROID] " + match[1]); + + var alertRe = /Emitting system alert message/; + if (alertRe.test(line)) { + logger.info("Emiting alert message..."); + me.webSocket.sockets.emit('alert', {message: line}); + } } else { // The dump command will always disconnect UiAutomation. // Detect the crash then restart UiAutomation. diff --git a/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java b/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java index f99997413..3f9e1a11b 100644 --- a/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java +++ b/android/bootstrap/src/io/appium/android/bootstrap/SocketServer.java @@ -1,8 +1,6 @@ package io.appium.android.bootstrap; -import io.appium.android.bootstrap.exceptions.AndroidCommandException; -import io.appium.android.bootstrap.exceptions.CommandTypeException; -import io.appium.android.bootstrap.exceptions.SocketServerException; +import org.json.JSONException; import java.io.BufferedReader; import java.io.IOException; @@ -11,7 +9,10 @@ import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; -import org.json.JSONException; +import io.appium.android.bootstrap.exceptions.AndroidCommandException; +import io.appium.android.bootstrap.exceptions.CommandTypeException; +import io.appium.android.bootstrap.exceptions.SocketServerException; +import io.appium.android.bootstrap.utils.TheWatchers; /** * The SocketServer class listens on a specific port for commands from Appium, @@ -26,6 +27,7 @@ class SocketServer { PrintWriter out; boolean keepListening; private final AndroidCommandExecutor executor; + private final TheWatchers watchers = TheWatchers.getInstance(); /** * Constructor @@ -108,7 +110,11 @@ class SocketServer { in = new BufferedReader(new InputStreamReader(client.getInputStream())); out = new PrintWriter(client.getOutputStream(), true); while (keepListening) { - handleClientData(); + if (in.ready()) { + handleClientData(); + } + Thread.sleep(100); + watchers.check(); } in.close(); out.close(); @@ -116,6 +122,8 @@ class SocketServer { Logger.info("Closed client connection"); } catch (final IOException e) { throw new SocketServerException("Error when client was trying to connect"); + } catch (InterruptedException e) { + throw new SocketServerException("The socket server was interupted"); } } @@ -123,8 +131,8 @@ class SocketServer { * When {@link #handleClientData()} has valid data, this method delegates the * command. * - * @param cmd - * @return + * @param cmd AndroidCommand + * @return Result */ private String runCommand(final AndroidCommand cmd) { AndroidCommandResult res; diff --git a/android/bootstrap/src/io/appium/android/bootstrap/utils/TheWatchers.java b/android/bootstrap/src/io/appium/android/bootstrap/utils/TheWatchers.java new file mode 100644 index 000000000..6e8c0f885 --- /dev/null +++ b/android/bootstrap/src/io/appium/android/bootstrap/utils/TheWatchers.java @@ -0,0 +1,48 @@ +package io.appium.android.bootstrap.utils; + +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiSelector; + +import io.appium.android.bootstrap.Logger; + +public class TheWatchers { + private static TheWatchers ourInstance = new TheWatchers(); + private long start = System.currentTimeMillis(); + private long delta = 1; + private boolean alerted = false; + + public static TheWatchers getInstance() { + return ourInstance; + } + + private TheWatchers() { + start = System.currentTimeMillis(); + } + + public void setDelta(long seconds) { + delta = seconds * 1000; + } + + public boolean check() { + if(start + delta < System.currentTimeMillis()) { + // Send only one alert message... + if (isDialogPresent() && (!alerted)) { + Logger.info("Emitting system alert message"); + alerted = true; + } + + // if the dialog went away, make sure we can send an alert again + if (!isDialogPresent() && alerted) { + alerted = false; + } + + start = System.currentTimeMillis(); + } + return false; + } + + public boolean isDialogPresent() { + UiObject alertDialog = new UiObject(new UiSelector().packageName("com.android.systemui")); + return alertDialog.exists(); + } +} diff --git a/app/android.js b/app/android.js index 921af0ba2..be0ffeb9b 100644 --- a/app/android.js +++ b/app/android.js @@ -19,6 +19,7 @@ var errors = require('./errors') var Android = function(opts) { this.rest = opts.rest; + this.webSocket = opts.webSocket; this.opts = opts; this.apkPath = opts.apkPath; this.udid = opts.udid; diff --git a/app/appium.js b/app/appium.js index b8002fd5d..35029affe 100644 --- a/app/appium.js +++ b/app/appium.js @@ -43,6 +43,7 @@ var Appium = function(args) { setWebhook(logger, port, host); } this.rest = null; + this.webSocket = null; this.devices = {}; this.deviceType = null; this.device = null; @@ -72,6 +73,14 @@ Appium.prototype.attachTo = function(rest, cb) { } }; +Appium.prototype.attachSocket = function(webSocket, cb) { + this.webSocket = webSocket; + + if (cb) { + cb(); + } +}; + Appium.prototype.registerConfig = function(configObj) { this.serverConfig = configObj; }; @@ -392,6 +401,7 @@ Appium.prototype.invoke = function() { if (this.isIos()) { var iosOpts = { rest: this.rest + , webSocket: this.webSocket , app: this.args.app , ipa: this.args.ipa , bundleId: this.args.bundleId @@ -410,6 +420,7 @@ Appium.prototype.invoke = function() { } else if (this.isAndroid()) { var androidOpts = { rest: this.rest + , webSocket: this.webSocket , apkPath: this.args.app , verbose: !this.args.quiet , udid: this.args.udid diff --git a/app/ios.js b/app/ios.js index 9f741495b..b100fdb5e 100644 --- a/app/ios.js +++ b/app/ios.js @@ -28,6 +28,7 @@ var path = require('path') var IOS = function(args) { this.rest = args.rest; + this.webSocket = args.webSocket; this.app = args.app; this.ipa = args.ipa; this.bundleId = args.bundleId || null; @@ -212,6 +213,7 @@ IOS.prototype.start = function(cb, onDie) { , path.resolve(__dirname, 'uiauto/Automation.tracetemplate') , sock , me.withoutDelay + , me.webSocket , onLaunch , onExit ); diff --git a/instruments/instruments.js b/instruments/instruments.js index ca0d187fa..2c45b09a1 100644 --- a/instruments/instruments.js +++ b/instruments/instruments.js @@ -11,12 +11,13 @@ var spawn = require('child_process').spawn , path = require('path') , codes = require('../app/uiauto/lib/status.js').codes; -var Instruments = function(app, udid, bootstrap, template, sock, withoutDelay, cb, exitCb) { +var Instruments = function(app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) { this.app = app; this.udid = udid; this.bootstrap = bootstrap; this.template = template; this.withoutDelay = withoutDelay; + this.webSocket = webSocket; this.commandQueue = []; this.curCommand = null; this.resultHandler = this.defaultResultHandler; @@ -277,6 +278,12 @@ Instruments.prototype.outputStreamHandler = function(output) { Instruments.prototype.errorStreamHandler = function(output) { logger.info(("[INST STDERR] " + output).yellow); + var re = /Call to onAlert returned 'YES'/; + var match = re.exec(output); + if (match) { + logger.info("Emiting alert message..."); + this.webSocket.sockets.emit('alert', {message: output}); + } }; Instruments.prototype.lookForShutdownInfo = function(output) { @@ -327,7 +334,7 @@ Instruments.prototype.debug = function(msg) { /* NODE EXPORTS */ -module.exports = function(server, app, udid, bootstrap, template, sock, withoutDelay, cb, exitCb) { - return new Instruments(server, app, udid, bootstrap, template, sock, withoutDelay, cb, exitCb); +module.exports = function(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb) { + return new Instruments(server, app, udid, bootstrap, template, sock, withoutDelay, webSocket, cb, exitCb); }; diff --git a/package.json b/package.json index f720beb5d..200aec02f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "node-idevice": "~0.1.2", "win-spawn" : "~1.1.1", "adm-zip" : "~0.4.3", - "ws": "0.4.25" + "ws": "0.4.25", + "socket.io" : "~0.9.14" }, "scripts": { "test": "grunt travis" @@ -71,6 +72,9 @@ "assert": "~0.4.9", "grunt-mocha-test": "~0.2.0", "grunt-contrib-jshint": "~0.1.1", - "saucelabs": "~0.0.7" + "saucelabs": "~0.0.7", + "socket.io-client": "~0.9.11", + "socks": "~0.0.1", + "telnetclient": "~0.0.1" } } diff --git a/sample-code/examples/python/alerts.py b/sample-code/examples/python/alerts.py new file mode 100644 index 000000000..d6bca5611 --- /dev/null +++ b/sample-code/examples/python/alerts.py @@ -0,0 +1,41 @@ +from socketIO_client import SocketIO + +""" +Please note that you will need to install the socketIO-client python module. + +This app will block on the wait() method until messages are recieved. It's +advised to read the documentation for socketIO-client and listen for alerts +in a seperate thread. + +This should not be considered production quality. +""" + + +class Alerts(object): + def __init__(self): + self.socket = SocketIO('localhost', 4723) + self.connect_events() + self.socket.wait() + + def connect_events(self): + self.socket.on('connect', self.on_connect) + self.socket.on('disconnect', self.on_disconnect) + self.socket.on('alert', self.on_alert) + self.socket.on('error', self.on_error) + + def on_connect(self): + print "[connected]" + + def on_alert(self, *args): + message = args[0]['message'] + print 'on_alert', str(message) + + def on_disconnect(self): + print "[disconnected]" + + def on_error(self, name, message): + print('Error: %s: %s' % (name, message)) + + +if __name__ == "__main__": + alerts = Alerts() diff --git a/server.js b/server.js index 994536d34..fa9fc84bd 100644 --- a/server.js +++ b/server.js @@ -12,6 +12,7 @@ var http = require('http') , async = require('async') , _ = require("underscore") , parser = require('./app/parser') + , io = require('socket.io') , gridRegister = require('./grid_register'); var allowCrossDomain = function(req, res, next) { @@ -49,7 +50,8 @@ var main = function(args, readyCb, doneCb) { doneCb = function() {}; } var rest = express() - , server = http.createServer(rest); + , server = http.createServer(rest) + , alerts = io.listen(server, {'flash policy port': -1}); rest.configure(function() { rest.use(express.favicon()); @@ -73,6 +75,24 @@ var main = function(args, readyCb, doneCb) { // Hook up REST http interface appiumServer.attachTo(rest); + alerts.configure(function() { + alerts.set('log level', 1); + alerts.set("polling duration", 10); + alerts.set("transports", ['websocket', 'flashsocket']); + }); + + alerts.sockets.on("connection", function (socket) { + socket.set('log level', 1); + logger.info("Client connected: " + (socket.id).toString()); + + socket.on('disconnect', function(data) { + logger.info("Client disconnected: " + data); + }); + }); + + // add web socket so we can emit events + appiumServer.attachSocket(alerts); + var checkSetup = function(cb) { var configFile = path.resolve(__dirname, ".appiumconfig"); fs.readFile(configFile, function(err, data) { diff --git a/test/functional/apidemos/alerts.js b/test/functional/apidemos/alerts.js new file mode 100644 index 000000000..509e4a76a --- /dev/null +++ b/test/functional/apidemos/alerts.js @@ -0,0 +1,60 @@ +/*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" + , describeWd = require("../../helpers/driverblock.js").describeForApp(appPath, + "android", appPkg, appAct) + , should = require('should') + , telnet = require('telnetclient') + , io = require('socket.io-client'); + + +describeWd('alert dialog detection', function(h) { + // setup websocket client... + var options ={ + transports: ['websocket'], + 'force new connection': true + }; + var client = io.connect('http://127.0.0.1:4723', options); + + // set up telnet... + var runCommands = function(cmds, cb) { + var client = new telnet.Client('127.0.0.1', 5554, {}, function(socket) { + }) + .on('data',function(d,e) {}) + .on('end',function() {}) + for (var i=0; i