Backed out changeset c2aa06a2ab49 (bug 1211489) for Gij(39) failures. r=backout

This commit is contained in:
Sebastian Hengst 2015-11-26 23:47:34 +01:00
parent 86b25d9035
commit e267a3bf1b
10 changed files with 436 additions and 588 deletions

View File

@ -0,0 +1,163 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("chrome://marionette/content/error.js");
this.EXPORTED_SYMBOLS = ["CommandProcessor", "Response"];
const logger = Log.repository.getLogger("Marionette");
const validator = {
exclusionary: {
"capabilities": ["error", "value"],
"error": ["value", "sessionId", "capabilities"],
"sessionId": ["error", "value"],
"value": ["error", "sessionId", "capabilities"],
},
set: function(obj, prop, val) {
let tests = this.exclusionary[prop];
if (tests) {
for (let t of tests) {
if (obj.hasOwnProperty(t)) {
throw new TypeError(`${t} set, cannot set ${prop}`);
}
}
}
obj[prop] = val;
return true;
},
};
/**
* The response body is exposed as an argument to commands.
* Commands can set fields on the body through defining properties.
*
* Setting properties invokes a validator that performs tests for
* mutually exclusionary fields on the input against the existing data
* in the body.
*
* For example setting the {@code error} property on the body when
* {@code value}, {@code sessionId}, or {@code capabilities} have been
* set previously will cause an error.
*/
this.ResponseBody = () => new Proxy({}, validator);
/**
* Represents the response returned from the remote end after execution
* of its corresponding command.
*
* The response is a mutable object passed to each command for
* modification through the available setters. To send data in a response,
* you modify the body property on the response. The body property can
* also be replaced completely.
*
* The response is sent implicitly by CommandProcessor when a command
* has finished executing, and any modifications made subsequent to that
* will have no effect.
*
* @param {number} cmdId
* UUID tied to the corresponding command request this is
* a response for.
* @param {function(Object, number)} respHandler
* Callback function called on responses.
*/
this.Response = function(cmdId, respHandler) {
this.id = cmdId;
this.respHandler = respHandler;
this.sent = false;
this.body = ResponseBody();
};
Response.prototype.send = function() {
if (this.sent) {
throw new RangeError("Response has already been sent: " + this.toString());
}
this.respHandler(this.body, this.id);
this.sent = true;
};
Response.prototype.sendError = function(err) {
let wd = error.isWebDriverError(err);
let we = wd ? err : new WebDriverError(err.message);
this.body.error = we.status;
this.body.message = we.message || null;
this.body.stacktrace = we.stack || null;
this.send();
// propagate errors that are implementation problems
if (!wd) {
throw err;
}
};
/**
* The command processor receives messages on execute(payload, )
* from the dispatcher, processes them, and wraps the functions that
* it executes from the WebDriver implementation, driver.
*
* @param {GeckoDriver} driver
* Reference to the driver implementation.
*/
this.CommandProcessor = function(driver) {
this.driver = driver;
};
/**
* Executes a WebDriver command based on the received payload,
* which is expected to be an object with a "parameters" property
* that is a simple key/value collection of arguments.
*
* The respHandler function will be called with the JSON object to
* send back to the client.
*
* The cmdId is the UUID tied to this request that prevents
* the dispatcher from sending responses in the wrong order.
*
* @param {Object} payload
* Message as received from client.
* @param {function(Object, number)} respHandler
* Callback function called on responses.
* @param {number} cmdId
* The unique identifier for the command to execute.
*/
CommandProcessor.prototype.execute = function(payload, respHandler, cmdId) {
let cmd = payload;
let resp = new Response(cmdId, respHandler);
let sendResponse = resp.send.bind(resp);
let sendError = resp.sendError.bind(resp);
// Ideally handlers shouldn't have to care about the command ID,
// but some methods (newSession, executeScript, et al.) have not
// yet been converted to use the new form of request dispatching.
cmd.id = cmdId;
let req = Task.spawn(function*() {
let fn = this.driver.commands[cmd.name];
if (typeof fn == "undefined") {
throw new UnknownCommandError(cmd.name);
}
let rv = yield fn.bind(this.driver)(cmd, resp);
if (typeof rv != "undefined") {
if (typeof rv != "object") {
resp.body = {value: rv};
} else {
resp.body = rv;
}
}
}.bind(this));
req.then(sendResponse, sendError).catch(error.report);
};

View File

@ -4,21 +4,23 @@
"use strict";
const {interfaces: Ci, utils: Cu} = Components;
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("chrome://marionette/content/driver.js");
Cu.import("chrome://marionette/content/command.js");
Cu.import("chrome://marionette/content/emulator.js");
Cu.import("chrome://marionette/content/error.js");
Cu.import("chrome://marionette/content/message.js");
Cu.import("chrome://marionette/content/driver.js");
this.EXPORTED_SYMBOLS = ["Dispatcher"];
const PROTOCOL_VERSION = 3;
const PROTOCOL_VERSION = 2;
const logger = Log.repository.getLogger("Marionette");
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
/**
* Manages a Marionette connection, and dispatches packets received to
@ -31,109 +33,111 @@ const logger = Log.repository.getLogger("Marionette");
* @param {function(Emulator): GeckoDriver} driverFactory
* A factory function that takes an Emulator as argument and produces
* a GeckoDriver.
* @param {function()} stopSignal
* Signal to stop the Marionette server.
*/
this.Dispatcher = function(connId, transport, driverFactory) {
this.connId = connId;
this.Dispatcher = function(connId, transport, driverFactory, stopSignal) {
this.id = connId;
this.conn = transport;
// transport hooks are Dispatcher#onPacket
// and Dispatcher#onClosed
this.conn.hooks = this;
// callback for when connection is closed
this.onclose = null;
// last received/sent message ID
this.lastId = 0;
// transport hooks are Dispatcher.prototype.onPacket
// and Dispatcher.prototype.onClosed
this.conn.hooks = this;
this.emulator = new Emulator(this.sendEmulator.bind(this));
this.emulator = new Emulator(msg => this.send(msg, -1));
this.driver = driverFactory(this.emulator);
this.commandProcessor = new CommandProcessor(this.driver);
// lookup of commands sent by server to client by message ID
this.commands_ = new Map();
this.stopSignal_ = stopSignal;
};
/**
* Debugger transport callback that dispatches the request.
* Request handlers defined in this.requests take presedence
* over those defined in this.driver.commands.
*/
Dispatcher.prototype.onPacket = function(packet) {
if (logger.level <= Log.Level.Debug) {
logger.debug(this.id + " -> " + JSON.stringify(packet));
}
if (this.requests && this.requests[packet.name]) {
this.requests[packet.name].bind(this)(packet);
} else {
let id = this.beginNewCommand();
let send = this.send.bind(this);
this.commandProcessor.execute(packet, send, id);
}
};
/**
* Debugger transport callback that cleans up
* after a connection is closed.
*/
Dispatcher.prototype.onClosed = function(reason) {
Dispatcher.prototype.onClosed = function(status) {
this.driver.sessionTearDown();
if (this.onclose) {
this.onclose(this);
}
};
/**
* Callback that receives data packets from the client.
*
* If the message is a Response, we look up the command previously issued
* to the client and run its callback, if any. In case of a Command,
* the corresponding is executed.
*
* @param {Array.<number, number, ?, ?>} data
* A four element array where the elements, in sequence, signifies
* message type, message ID, method name or error, and parameters
* or result.
*/
Dispatcher.prototype.onPacket = function(data) {
let msg = Message.fromMsg(data);
msg.origin = MessageOrigin.Client;
this.log_(msg);
// Dispatcher specific command handlers:
if (msg instanceof Response) {
let cmd = this.commands_.get(msg.id);
this.commands_.delete(msg.id);
cmd.onresponse(msg);
} else if (msg instanceof Command) {
this.lastId = msg.id;
this.execute(msg);
Dispatcher.prototype.emulatorCmdResult = function(msg) {
switch (this.driver.context) {
case Context.CONTENT:
this.driver.sendAsync("emulatorCmdResult", msg);
break;
case Context.CHROME:
let cb = this.emulator.popCallback(msg.id);
if (!cb) {
return;
}
cb.result(msg);
break;
}
};
/**
* Executes a WebDriver command and sends back a response when it has
* finished executing.
*
* Commands implemented in GeckoDriver and registered in its
* {@code GeckoDriver.commands} attribute. The return values from
* commands are expected to be Promises. If the resolved value of said
* promise is not an object, the response body will be wrapped in an object
* under a "value" field.
*
* If the command implementation sends the response itself by calling
* {@code resp.send()}, the response is guaranteed to not be sent twice.
*
* Errors thrown in commands are marshaled and sent back, and if they
* are not WebDriverError instances, they are additionally propagated and
* reported to {@code Components.utils.reportError}.
*
* @param {Command} cmd
* The requested command to execute.
* Quits Firefox with the provided flags and tears down the current
* session.
*/
Dispatcher.prototype.execute = function(cmd) {
let resp = new Response(cmd.id, this.send.bind(this));
let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
let sendError = resp.sendError.bind(resp);
Dispatcher.prototype.quitApplication = function(msg) {
let id = this.beginNewCommand();
let req = Task.spawn(function*() {
let fn = this.driver.commands[cmd.name];
if (typeof fn == "undefined") {
throw new UnknownCommandError(cmd.name);
}
if (this.driver.appName != "Firefox") {
this.sendError(new WebDriverError("In app initiated quit only supported in Firefox"));
return;
}
let rv = yield fn.bind(this.driver)(cmd, resp);
let flags = Ci.nsIAppStartup.eAttemptQuit;
for (let k of msg.parameters.flags) {
flags |= Ci.nsIAppStartup[k];
}
if (typeof rv != "undefined") {
if (typeof rv != "object") {
resp.body = {value: rv};
} else {
resp.body = rv;
}
}
}.bind(this));
this.stopSignal_();
this.sendOk(id);
req.then(sendResponse, sendError).catch(error.report);
this.driver.sessionTearDown();
Services.startup.quit(flags);
};
// Convenience methods:
Dispatcher.prototype.sayHello = function() {
let id = this.beginNewCommand();
let whatHo = {
applicationType: "gecko",
marionetteProtocol: PROTOCOL_VERSION,
};
this.send(whatHo, id);
};
Dispatcher.prototype.sendOk = function(cmdId) {
this.send({}, cmdId);
};
Dispatcher.prototype.sendError = function(err, cmdId) {
@ -141,30 +145,6 @@ Dispatcher.prototype.sendError = function(err, cmdId) {
resp.sendError(err);
};
// Convenience methods:
/**
* When a client connects we send across a JSON Object defining the
* protocol level.
*
* This is the only message sent by Marionette that does not follow
* the regular message format.
*/
Dispatcher.prototype.sayHello = function() {
let whatHo = {
applicationType: "gecko",
marionetteProtocol: PROTOCOL_VERSION,
};
this.sendRaw(whatHo);
};
Dispatcher.prototype.sendEmulator = function(name, params, resCb, errCb) {
let cmd = new Command(++this.lastId, name, params);
cmd.onresult = resCb;
cmd.onerror = errCb;
this.send(cmd);
};
/**
* Delegates message to client or emulator based on the provided
* {@code cmdId}. The message is sent over the debugger transport socket.
@ -176,69 +156,83 @@ Dispatcher.prototype.sendEmulator = function(name, params, resCb, errCb) {
* correct order, emulator callbacks are more transparent and can be sent
* at any time. These callbacks won't change the current command state.
*
* @param {Command,Response} msg
* The command or response to send.
* @param {Object} payload
* The payload to send.
* @param {UUID} cmdId
* The unique identifier for this payload. {@code -1} signifies
* that it's an emulator callback.
*/
Dispatcher.prototype.send = function(msg) {
msg.origin = MessageOrigin.Server;
if (msg instanceof Command) {
this.commands_.set(msg.id, msg);
this.sendToEmulator(msg);
} else if (msg instanceof Response) {
this.sendToClient(msg);
Dispatcher.prototype.send = function(payload, cmdId) {
if (emulator.isCallback(cmdId)) {
this.sendToEmulator(payload);
} else {
this.sendToClient(payload, cmdId);
this.commandId = null;
}
};
// Low-level methods:
/**
* Send command to emulator over the debugger transport socket.
*
* @param {Command} cmd
* The command to issue to the emulator.
* Send message to emulator over the debugger transport socket.
* Notably this skips out-of-sync command checks.
*/
Dispatcher.prototype.sendToEmulator = function(cmd) {
this.sendMessage(cmd);
Dispatcher.prototype.sendToEmulator = function(payload) {
this.sendRaw("emulator", payload);
};
/**
* Send given response to the client over the debugger transport socket.
* Send given payload as-is to the connected client over the debugger
* transport socket.
*
* @param {Response} resp
* The response to send back to the client.
* If {@code cmdId} evaluates to false, the current command state isn't
* set, or the response is out-of-sync, a warning is logged and this
* routine will return (no-op).
*/
Dispatcher.prototype.sendToClient = function(resp) {
Dispatcher.prototype.sendToClient = function(payload, cmdId) {
if (!cmdId) {
logger.warn("Got response with no command ID");
return;
} else if (this.commandId === null) {
logger.warn(`No current command, ignoring response: ${payload.toSource}`);
return;
} else if (this.isOutOfSync(cmdId)) {
logger.warn(`Ignoring out-of-sync response with command ID: ${cmdId}`);
return;
}
this.driver.responseCompleted();
this.sendMessage(resp);
this.sendRaw("client", payload);
};
/**
* Marshal message to the Marionette message format and send it.
*
* @param {Command,Response} msg
* The message to send.
* Sends payload as-is over debugger transport socket to client,
* and logs it.
*/
Dispatcher.prototype.sendMessage = function(msg) {
this.log_(msg);
let payload = msg.toMsg();
this.sendRaw(payload);
};
/**
* Send the given payload over the debugger transport socket to the
* connected client.
*
* @param {Object} payload
* The payload to ship.
*/
Dispatcher.prototype.sendRaw = function(payload) {
Dispatcher.prototype.sendRaw = function(dest, payload) {
if (logger.level <= Log.Level.Debug) {
logger.debug(this.id + " " + dest + " <- " + JSON.stringify(payload));
}
this.conn.send(payload);
};
Dispatcher.prototype.log_ = function(msg) {
if (logger.level > Log.Level.Debug) {
return;
}
let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- ");
logger.debug(this.connId + a + msg);
/**
* Begins a new command by generating a unique identifier and assigning
* it to the current command state {@code Dispatcher.prototype.commandId}.
*
* @return {UUID}
* The generated unique identifier for the current command.
*/
Dispatcher.prototype.beginNewCommand = function() {
let uuid = uuidGen.generateUUID().toString();
this.commandId = uuid;
return uuid;
};
Dispatcher.prototype.isOutOfSync = function(cmdId) {
return this.commandId !== cmdId;
};
Dispatcher.prototype.requests = {
emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult,
quitApplication: Dispatcher.prototype.quitApplication
};

View File

@ -24,6 +24,7 @@ XPCOMUtils.defineLazyServiceGetter(
Cu.import("chrome://marionette/content/actions.js");
Cu.import("chrome://marionette/content/elements.js");
Cu.import("chrome://marionette/content/emulator.js");
Cu.import("chrome://marionette/content/error.js");
Cu.import("chrome://marionette/content/modal.js");
Cu.import("chrome://marionette/content/proxy.js");
@ -94,17 +95,12 @@ this.Context.fromString = function(s) {
* Description of the product, for example "B2G" or "Firefox".
* @param {string} device
* Device this driver should assume.
* @param {function()} stopSignal
* Signal to stop the Marionette server.
* @param {Emulator=} emulator
* Reference to the emulator connection, if running on an emulator.
*/
this.GeckoDriver = function(appName, device, stopSignal, emulator) {
this.GeckoDriver = function(appName, device, emulator) {
this.appName = appName;
this.stopSignal_ = stopSignal;
this.emulator = emulator;
// TODO(ato): hack
this.emulator.sendToListener = this.sendAsync.bind(this);
this.sessionId = null;
// holds list of BrowserObjs
@ -168,7 +164,6 @@ this.GeckoDriver = function(appName, device, stopSignal, emulator) {
this.mm = globalMessageManager;
this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this));
// always keep weak reference to current dialogue
this.dialog = null;
let handleDialog = (subject, topic) => {
let winr;
@ -1088,6 +1083,11 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
let res = yield new Promise(function(resolve, reject) {
let chromeAsyncReturnFunc = function(val) {
if (that.emulator.cbs.length > 0) {
that.emulator.cbs = [];
throw new WebDriverError("Emulator callback still pending when finish() called");
}
if (cmd.id == that.sandboxes[sandboxName].command_id) {
if (that.timer !== null) {
that.timer.cancel();
@ -1133,11 +1133,20 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
}
this.sandboxes[sandboxName].command_id = cmd.id;
this.sandboxes[sandboxName].runEmulatorCmd =
(cmd, cb) => this.emulator.command(cmd, cb, chromeAsyncError);
this.sandboxes[sandboxName].runEmulatorShell =
(args, cb) => this.emulator.shell(args, cb, chromeAsyncError);
this.sandboxes[sandboxName].runEmulatorCmd = (cmd, cb) => {
let ecb = new EmulatorCallback();
ecb.onresult = cb;
ecb.onerror = chromeAsyncError;
this.emulator.pushCallback(ecb);
this.emulator.send({emulator_cmd: cmd, id: ecb.id});
};
this.sandboxes[sandboxName].runEmulatorShell = (args, cb) => {
let ecb = new EmulatorCallback();
ecb.onresult = cb;
ecb.onerror = chromeAsyncError;
this.emulator.pushCallback(ecb);
this.emulator.send({emulator_shell: args, id: ecb.id});
};
this.applyArgumentsToSandbox(win, this.sandboxes[sandboxName], args);
// NB: win.onerror is not hooked by default due to the inability to
@ -2791,27 +2800,6 @@ GeckoDriver.prototype.sendKeysToDialog = function(cmd, resp) {
true /* ignore visibility check */);
};
/**
* Quits Firefox with the provided flags and tears down the current
* session.
*/
GeckoDriver.prototype.quitApplication = function(cmd, resp) {
if (this.appName != "Firefox") {
throw new WebDriverError("In app initiated quit only supported in Firefox");
}
let flags = Ci.nsIAppStartup.eAttemptQuit;
for (let k of cmd.parameters.flags) {
flags |= Ci.nsIAppStartup[k];
}
this.stopSignal_();
resp.send();
this.sessionTearDown();
Services.startup.quit(flags);
};
/**
* Helper function to convert an outerWindowID into a UID that Marionette
* tracks.
@ -2847,6 +2835,11 @@ GeckoDriver.prototype.receiveMessage = function(message) {
}
break;
case "Marionette:runEmulatorCmd":
case "Marionette:runEmulatorShell":
this.emulator.send(message.json);
break;
case "Marionette:switchToModalOrigin":
this.curBrowser.frameManager.switchToModalOrigin(message);
this.mm = this.curBrowser.frameManager
@ -3029,8 +3022,7 @@ GeckoDriver.prototype.commands = {
"dismissDialog": GeckoDriver.prototype.dismissDialog,
"acceptDialog": GeckoDriver.prototype.acceptDialog,
"getTextFromDialog": GeckoDriver.prototype.getTextFromDialog,
"sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog,
"quitApplication": GeckoDriver.prototype.quitApplication,
"sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog
};
/**

View File

@ -4,14 +4,18 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
var {classes: Cc, interfaces: Ci} = Components;
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
this.EXPORTED_SYMBOLS = ["emulator", "Emulator", "EmulatorCallback"];
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.emulator = {};
const logger = Log.repository.getLogger("Marionette");
this.EXPORTED_SYMBOLS = ["Emulator"];
/**
* Determines if command ID is an emulator callback.
*/
this.emulator.isCallback = function(cmdId) {
return cmdId < 0;
};
/**
* Represents the connection between Marionette and the emulator it's
@ -22,90 +26,97 @@ this.EXPORTED_SYMBOLS = ["Emulator"];
* which is stored in cbs. They are later retreived by their unique ID
* using popCallback.
*
* @param {function(Object)} sendToEmulatorFn
* @param {function(Object)} sendFn
* Callback function that sends a message to the emulator.
* @param {function(Object)} sendToEmulatorFn
* Callback function that sends a message asynchronously to the
* current listener.
*/
this.Emulator = function(sendToEmulatorFn) {
this.sendToEmulator = sendToEmulatorFn;
this.Emulator = function(sendFn) {
this.send = sendFn;
this.cbs = [];
};
/**
* Instruct the client to run an Android emulator command.
* Pops a callback off the stack if found. Otherwise this is a no-op.
*
* @param {string} cmd
* The command to run.
* @param {function(?)} resCb
* Callback on a result response from the emulator.
* @param {function(?)} errCb
* Callback on an error in running the command.
*/
Emulator.prototype.command = function(cmd, resCb, errCb) {
assertDefined(cmd, "runEmulatorCmd");
this.sendToEmulator(
"runEmulatorCmd", {emulator_cmd: cmd}, resCb, errCb);
};
/**
* Instruct the client to execute Android emulator shell arguments.
* @param {number} id
* Unique ID associated with the callback.
*
* @param {Array.<string>} args
* The shell instruction for the emulator to execute.
* @param {function(?)} resCb
* Callback on a result response from the emulator.
* @param {function(?)} errCb
* Callback on an error in executing the shell arguments.
* @return {?function(Object)}
* Callback function that takes an emulator response message as
* an argument.
*/
Emulator.prototype.shell = function(args, resCb, errCb) {
assertDefined(args, "runEmulatorShell");
this.sendToEmulator(
"runEmulatorShell", {emulator_shell: args}, resCb, errCb);
};
Emulator.prototype.processMessage = function(msg) {
let resCb = this.resultCallback(msg.json.id);
let errCb = this.errorCallback(msg.json.id);
switch (msg.name) {
case "Marionette:runEmulatorCmd":
this.command(msg.json.command, resCb, errCb);
break;
case "Marionette:runEmulatorShell":
this.shell(msg.json.arguments, resCb, errCb);
break;
Emulator.prototype.popCallback = function(id) {
let f, fi;
for (let i = 0; i < this.cbs.length; ++i) {
if (this.cbs[i].id == id) {
f = this.cbs[i];
fi = i;
}
}
if (!f) {
return null;
}
this.cbs.splice(fi, 1);
return f;
};
Emulator.prototype.resultCallback = function(msgId) {
return res => this.sendResult({result: res, id: msgId});
/**
* Pushes callback on to the stack.
*
* @param {function(Object)} cb
* Callback function that takes an emulator response message as
* an argument.
*/
Emulator.prototype.pushCallback = function(cb) {
cb.send_ = this.sendFn;
this.cbs.push(cb);
};
Emulator.prototype.errorCallback = function(msgId) {
return err => this.sendResult({error: err, id: msgId});
/**
* Encapsulates a callback to the emulator and provides an execution
* environment for them.
*
* Each callback is assigned a unique identifier, id, that can be used
* to retrieve them from Emulator's stack using popCallback.
*
* The onresult event listener is triggered when a result arrives on
* the callback.
*
* The onerror event listener is triggered when an error occurs during
* the execution of that callback.
*/
this.EmulatorCallback = function() {
this.id = uuidGen.generateUUID().toString();
this.onresult = null;
this.onerror = null;
this.send_ = null;
};
Emulator.prototype.sendResult = function(msg) {
// sendToListener set explicitly in GeckoDriver's ctor
this.sendToListener("emulatorCmdResult", msg);
EmulatorCallback.prototype.command = function(cmd, cb) {
this.onresult = cb;
this.send_({emulator_cmd: cmd, id: this.id});
};
/** Receives IPC messages from the listener. */
Emulator.prototype.receiveMessage = function(msg) {
EmulatorCallback.prototype.shell = function(args, cb) {
this.onresult = cb;
this.send_({emulator_shell: args, id: this.id});
};
EmulatorCallback.prototype.result = function(msg) {
if (this.send_ === null) {
throw new TypeError(
"EmulatorCallback must be registered with Emulator to fire");
}
try {
this.processMessage(msg);
if (!this.onresult) {
return;
}
this.onresult(msg.result);
} catch (e) {
this.sendResult({error: `${e.name}: ${e.message}`, id: msg.json.id});
if (this.onerror) {
this.onerror(e);
}
}
};
Emulator.prototype.QueryInterface = XPCOMUtils.generateQI(
[Ci.nsIMessageListener, Ci.nsISupportsWeakReference]);
function assertDefined(arg, action) {
if (typeof arg == "undefined") {
throw new TypeError("Not enough arguments to " + action);
}
}

View File

@ -109,24 +109,6 @@ error.stringify = function(err) {
}
};
/**
* Marshal an Error to a JSON structure.
*
* @param {Error} err
* The Error to serialise.
*
* @return {Object.<string, Object>}
* JSON structure with the keys "error", "message", and "stacktrace".
*/
error.toJson = function(err) {
let json = {
error: err.status,
message: err.message || null,
stacktrace: err.stack || null,
};
return json;
};
/**
* WebDriverError is the prototypal parent of all WebDriver errors.
* It should not be used directly, as it does not correspond to a real
@ -234,7 +216,7 @@ this.NoAlertOpenError = function(msg) {
WebDriverError.call(this, msg);
this.name = "NoAlertOpenError";
this.status = "no such alert";
};
}
NoAlertOpenError.prototype = Object.create(WebDriverError.prototype);
this.NoSuchElementError = function(msg) {

View File

@ -185,8 +185,8 @@ FrameManager.prototype = {
mm.addWeakMessageListener("Marionette:error", this.server);
mm.addWeakMessageListener("Marionette:emitTouchEvent", this.server);
mm.addWeakMessageListener("Marionette:log", this.server);
mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.server);
mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server);
mm.addWeakMessageListener("Marionette:shareData", this.server);
mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.server);
mm.addWeakMessageListener("Marionette:switchedToFrame", this.server);
@ -217,8 +217,8 @@ FrameManager.prototype = {
mm.removeWeakMessageListener("Marionette:error", this.server);
mm.removeWeakMessageListener("Marionette:log", this.server);
mm.removeWeakMessageListener("Marionette:shareData", this.server);
mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server);
mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server);
mm.removeWeakMessageListener("Marionette:switchedToFrame", this.server);
mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.server);
mm.removeWeakMessageListener("Marionette:listenersAttached", this.server);

View File

@ -16,7 +16,7 @@ marionette.jar:
content/EventUtils.js (EventUtils.js)
content/ChromeUtils.js (ChromeUtils.js)
content/error.js (error.js)
content/message.js (message.js)
content/command.js (command.js)
content/dispatcher.js (dispatcher.js)
content/emulator.js (emulator.js)
content/modal.js (modal.js)

View File

@ -1903,48 +1903,40 @@ function getAppCacheStatus(msg) {
}
// emulator callbacks
var _emu_cb_id = 0;
var _emu_cbs = {};
function runEmulatorCmd(cmd, callback) {
logger.info("listener runEmulatorCmd cmd=" + cmd);
if (callback) {
_emu_cbs[asyncTestCommandId] = callback;
_emu_cbs[_emu_cb_id] = callback;
}
sendAsyncMessage("Marionette:runEmulatorCmd",
{command: cmd, id: asyncTestCommandId});
sendAsyncMessage("Marionette:runEmulatorCmd", {emulator_cmd: cmd, id: _emu_cb_id});
_emu_cb_id += 1;
}
function runEmulatorShell(args, callback) {
if (callback) {
_emu_cbs[asyncTestCommandId] = callback;
_emu_cbs[_emu_cb_id] = callback;
}
sendAsyncMessage("Marionette:runEmulatorShell",
{arguments: args, id: asyncTestCommandId});
sendAsyncMessage("Marionette:runEmulatorShell", {emulator_shell: args, id: _emu_cb_id});
_emu_cb_id += 1;
}
function emulatorCmdResult(msg) {
let {error, result, id} = msg.json;
if (error) {
let err = new JavaScriptError(error);
sendError(err, id);
return;
}
let message = msg.json;
if (!sandboxes[sandboxName]) {
return;
}
let cb = _emu_cbs[id];
delete _emu_cbs[id];
let cb = _emu_cbs[message.id];
delete _emu_cbs[message.id];
if (!cb) {
return;
}
try {
cb(result);
cb(message.result);
} catch (e) {
let err = new JavaScriptError(e);
sendError(err, id);
sendError(e, -1);
return;
}
}

View File

@ -1,288 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("chrome://marionette/content/error.js");
this.EXPORTED_SYMBOLS = [
"Command",
"Message",
"MessageOrigin",
"Response",
];
const logger = Log.repository.getLogger("Marionette");
this.MessageOrigin = {
Client: 0,
Server: 1,
};
this.Message = {};
/**
* Converts a data packet into a Command or Response type.
*
* @param {Array.<number, number, ?, ?>} data
* A four element array where the elements, in sequence, signifies
* message type, message ID, method name or error, and parameters
* or result.
*
* @return {(Command,Response)}
* Based on the message type, a Command or Response instance.
*
* @throws {TypeError}
* If the message type is not recognised.
*/
Message.fromMsg = function(data) {
switch (data[0]) {
case Command.TYPE:
return Command.fromMsg(data);
case Response.TYPE:
return Response.fromMsg(data);
default:
throw new TypeError(
"Unrecognised message type in packet: " + JSON.stringify(data));
}
};
/**
* A command is a request from the client to run a series of remote end
* steps and return a fitting response.
*
* The command can be synthesised from the message passed over the
* Marionette socket using the {@code fromMsg} function. The format of
* a message is:
*
* [type, id, name, params]
*
* where
*
* type:
* Must be zero (integer). Zero means that this message is a command.
*
* id:
* Number used as a sequence number. The server replies with a
* requested id.
*
* name:
* String representing the command name with an associated set of
* remote end steps.
*
* params:
* Object of command function arguments. The keys of this object
* must be strings, but the values can be arbitrary values.
*
* A command has an associated message {@code id} that prevents the
* dispatcher from sending responses in the wrong order.
*
* The command may also have optional error- and result handlers that
* are called when the client returns with a response. These are
* {@code function onerror({Object})}, {@code function onresult({Object})},
* and {@code function onresult({Response})}.
*
* @param {number} msgId
* Message ID unique identifying this message.
* @param {string} name
* Command name.
* @param {Object<string, ?>} params
* Command parameters.
*/
this.Command = class {
constructor(msgId, name, params={}) {
this.id = msgId;
this.name = name;
this.parameters = params;
this.onerror = null;
this.onresult = null;
this.origin = MessageOrigin.Client;
this.sent = false;
}
/**
* Calls the error- or result handler associated with this command.
* This function can be replaced with a custom response handler.
*
* @param {Response} resp
* The response to pass on to the result or error to the
* {@code onerror} or {@code onresult} handlers to.
*/
onresponse(resp) {
if (resp.error && this.onerror) {
this.onerror(resp.error);
} else if (resp.body && this.onresult) {
this.onresult(resp.body);
}
}
toMsg() {
return [Command.TYPE, this.id, this.name, this.parameters];
}
toString() {
return "Command {id: " + this.id + ", " +
"name: " + JSON.stringify(this.name) + ", " +
"parameters: " + JSON.stringify(this.parameters) + "}"
}
static fromMsg(msg) {
return new Command(msg[1], msg[2], msg[3]);
}
};
Command.TYPE = 0;
const validator = {
exclusionary: {
"capabilities": ["error", "value"],
"error": ["value", "sessionId", "capabilities"],
"sessionId": ["error", "value"],
"value": ["error", "sessionId", "capabilities"],
},
set: function(obj, prop, val) {
let tests = this.exclusionary[prop];
if (tests) {
for (let t of tests) {
if (obj.hasOwnProperty(t)) {
throw new TypeError(`${t} set, cannot set ${prop}`);
}
}
}
obj[prop] = val;
return true;
},
};
/**
* The response body is exposed as an argument to commands.
* Commands can set fields on the body through defining properties.
*
* Setting properties invokes a validator that performs tests for
* mutually exclusionary fields on the input against the existing data
* in the body.
*
* For example setting the {@code error} property on the body when
* {@code value}, {@code sessionId}, or {@code capabilities} have been
* set previously will cause an error.
*/
this.ResponseBody = () => new Proxy({}, validator);
/**
* Represents the response returned from the remote end after execution
* of its corresponding command.
*
* The response is a mutable object passed to each command for
* modification through the available setters. To send data in a response,
* you modify the body property on the response. The body property can
* also be replaced completely.
*
* The response is sent implicitly by CommandProcessor when a command
* has finished executing, and any modifications made subsequent to that
* will have no effect.
*
* @param {number} msgId
* Message ID tied to the corresponding command request this is a
* response for.
* @param {function(Response|Message)} respHandler
* Function callback called on sending the response.
*/
this.Response = class {
constructor(msgId, respHandler) {
this.id = msgId;
this.error = null;
this.body = ResponseBody();
this.origin = MessageOrigin.Server;
this.sent = false;
this.respHandler_ = respHandler;
}
/**
* Sends response conditionally, given a predicate.
*
* @param {function(Response): boolean} predicate
* A predicate taking a Response object and returning a boolean.
*/
sendConditionally(predicate) {
if (predicate(this)) {
this.send();
}
}
/**
* Sends response using the response handler provided on construction.
*
* @throws {RangeError}
* If the response has already been sent.
*/
send() {
if (this.sent) {
throw new RangeError("Response has already been sent: " + this);
}
this.respHandler_(this);
this.sent = true;
}
/**
* Send given Error to client.
*
* Turns the response into an error response, clears any previously
* set body data, and sends it using the response handler provided
* on construction.
*
* @param {Error} err
* The Error instance to send.
*
* @throws {Error}
* If the {@code error} is not a WebDriverError, the error is
* propagated.
*/
sendError(err) {
let wd = error.isWebDriverError(err);
let we = wd ? err : new WebDriverError(err.message);
this.error = error.toJson(err);
this.body = null;
this.send();
// propagate errors that are implementation problems
if (!wd) {
throw err;
}
}
toMsg() {
return [Response.TYPE, this.id, this.error, this.body];
}
toString() {
return "Response {id: " + this.id + ", " +
"error: " + JSON.stringify(this.error) + ", " +
"body: " + JSON.stringify(this.body) + "}";
}
static fromMsg(msg) {
let resp = new Response(msg[1], null);
resp.error = msg[2];
resp.body = msg[3];
return resp;
}
};
Response.TYPE = 1;

View File

@ -93,8 +93,7 @@ MarionetteServer.prototype.driverFactory = function(emulator) {
Services.io.offline = false;
}
let stopSignal = () => this.stop();
return new GeckoDriver(appName, device, stopSignal, emulator);
return new GeckoDriver(appName, device, emulator);
};
MarionetteServer.prototype.start = function() {
@ -130,17 +129,20 @@ MarionetteServer.prototype.onSocketAccepted = function(
let transport = new DebuggerTransport(input, output);
let connId = "conn" + this.nextConnId++;
let dispatcher = new Dispatcher(connId, transport, this.driverFactory.bind(this));
let stopSignal = () => this.stop();
let dispatcher = new Dispatcher(connId, transport, this.driverFactory, stopSignal);
dispatcher.onclose = this.onConnectionClosed.bind(this);
this.conns[connId] = dispatcher;
logger.info(`Accepted connection ${connId} from ${clientSocket.host}:${clientSocket.port}`);
// Create a root actor for the connection and send the hello packet
dispatcher.sayHello();
transport.ready();
};
MarionetteServer.prototype.onConnectionClosed = function(conn) {
let id = conn.connId;
let id = conn.id;
delete this.conns[id];
logger.info(`Closed connection ${id}`);
};