зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1211489: Provide message sequencing in Marionette
Message sequencing allows Marionette to provide an asynchronous, parallel pipelining user-facing interface, limit chances of payload race conditions, and remove stylistic inconsistencies in how commands and responses are dispatched internally. Clients that deliver a blocking WebDriver interface are still be expected to not send further command requests before the response from the last command has come back, but if they still happen to do so because of programming error or otherwise, no harm will be done. This will guard against bugs such as bug 1207125. This patch formalises the command and response concepts, and applies these concepts to emulator callbacks. Through the new message format, Marionette is able to provide two-way parallel communication. In other words, the server will be able to instruct the client to perform a command in a non ad-hoc way. runEmulatorCmd and runEmulatorShell are both turned into command instructions originating from the server. This resolves a lot of technical debt in the server code because they are no longer special-cased to circumvent the dispatching technique used for all other commands; commands may originate from either the client or the server providing parallel pipelining enforced through message sequencing: client server | | msgid=1 |----------->| | command | | | msgid=2 |<-----------| | command | | | msgid=2 |----------->| | response | | | msgid=1 |<-----------| | response | | | The protocol now consists of a "Command" message and the corresponding "Response" message. A "Response" message must always be sent in reply to a "Command" message. This bumps the Marionette protocol level to 3. r=dburns r=jgriffin --HG-- extra : commitid : 2upWRuXyqPF extra : rebase_source : f384801e209e4b49ef57055fd550c3c435ece4ef
This commit is contained in:
Родитель
f363c7dd2d
Коммит
86bd46756a
|
@ -1,163 +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 = ["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);
|
||||
};
|
|
@ -4,23 +4,21 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
const {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/command.js");
|
||||
Cu.import("chrome://marionette/content/driver.js");
|
||||
Cu.import("chrome://marionette/content/emulator.js");
|
||||
Cu.import("chrome://marionette/content/error.js");
|
||||
Cu.import("chrome://marionette/content/driver.js");
|
||||
Cu.import("chrome://marionette/content/message.js");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Dispatcher"];
|
||||
|
||||
const PROTOCOL_VERSION = 2;
|
||||
const PROTOCOL_VERSION = 3;
|
||||
|
||||
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
|
||||
|
@ -33,111 +31,109 @@ const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerat
|
|||
* @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, stopSignal) {
|
||||
this.id = connId;
|
||||
this.Dispatcher = function(connId, transport, driverFactory) {
|
||||
this.connId = 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;
|
||||
|
||||
// transport hooks are Dispatcher.prototype.onPacket
|
||||
// and Dispatcher.prototype.onClosed
|
||||
this.conn.hooks = this;
|
||||
// last received/sent message ID
|
||||
this.lastId = 0;
|
||||
|
||||
this.emulator = new Emulator(msg => this.send(msg, -1));
|
||||
this.emulator = new Emulator(this.sendEmulator.bind(this));
|
||||
this.driver = driverFactory(this.emulator);
|
||||
this.commandProcessor = new CommandProcessor(this.driver);
|
||||
|
||||
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);
|
||||
}
|
||||
// lookup of commands sent by server to client by message ID
|
||||
this.commands_ = new Map();
|
||||
};
|
||||
|
||||
/**
|
||||
* Debugger transport callback that cleans up
|
||||
* after a connection is closed.
|
||||
*/
|
||||
Dispatcher.prototype.onClosed = function(status) {
|
||||
Dispatcher.prototype.onClosed = function(reason) {
|
||||
this.driver.sessionTearDown();
|
||||
if (this.onclose) {
|
||||
this.onclose(this);
|
||||
}
|
||||
};
|
||||
|
||||
// Dispatcher specific command handlers:
|
||||
/**
|
||||
* 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.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;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Quits Firefox with the provided flags and tears down the current
|
||||
* session.
|
||||
* 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.
|
||||
*/
|
||||
Dispatcher.prototype.quitApplication = function(msg) {
|
||||
let id = this.beginNewCommand();
|
||||
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);
|
||||
|
||||
if (this.driver.appName != "Firefox") {
|
||||
this.sendError(new WebDriverError("In app initiated quit only supported in Firefox"));
|
||||
return;
|
||||
}
|
||||
let req = Task.spawn(function*() {
|
||||
let fn = this.driver.commands[cmd.name];
|
||||
if (typeof fn == "undefined") {
|
||||
throw new UnknownCommandError(cmd.name);
|
||||
}
|
||||
|
||||
let flags = Ci.nsIAppStartup.eAttemptQuit;
|
||||
for (let k of msg.parameters.flags) {
|
||||
flags |= Ci.nsIAppStartup[k];
|
||||
}
|
||||
let rv = yield fn.bind(this.driver)(cmd, resp);
|
||||
|
||||
this.stopSignal_();
|
||||
this.sendOk(id);
|
||||
if (typeof rv != "undefined") {
|
||||
if (typeof rv != "object") {
|
||||
resp.body = {value: rv};
|
||||
} else {
|
||||
resp.body = rv;
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
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);
|
||||
req.then(sendResponse, sendError).catch(error.report);
|
||||
};
|
||||
|
||||
Dispatcher.prototype.sendError = function(err, cmdId) {
|
||||
|
@ -145,6 +141,30 @@ 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.
|
||||
|
@ -156,83 +176,69 @@ Dispatcher.prototype.sendError = function(err, cmdId) {
|
|||
* correct order, emulator callbacks are more transparent and can be sent
|
||||
* at any time. These callbacks won't change the current command state.
|
||||
*
|
||||
* @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.
|
||||
* @param {Command,Response} msg
|
||||
* The command or response to send.
|
||||
*/
|
||||
Dispatcher.prototype.send = function(payload, cmdId) {
|
||||
if (emulator.isCallback(cmdId)) {
|
||||
this.sendToEmulator(payload);
|
||||
} else {
|
||||
this.sendToClient(payload, cmdId);
|
||||
this.commandId = null;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// Low-level methods:
|
||||
|
||||
/**
|
||||
* Send message to emulator over the debugger transport socket.
|
||||
* Notably this skips out-of-sync command checks.
|
||||
*/
|
||||
Dispatcher.prototype.sendToEmulator = function(payload) {
|
||||
this.sendRaw("emulator", payload);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send given payload as-is to the connected client over the debugger
|
||||
* transport socket.
|
||||
* Send command to emulator over the debugger transport socket.
|
||||
*
|
||||
* 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).
|
||||
* @param {Command} cmd
|
||||
* The command to issue to the emulator.
|
||||
*/
|
||||
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.sendRaw("client", payload);
|
||||
Dispatcher.prototype.sendToEmulator = function(cmd) {
|
||||
this.sendMessage(cmd);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends payload as-is over debugger transport socket to client,
|
||||
* and logs it.
|
||||
* Send given response to the client over the debugger transport socket.
|
||||
*
|
||||
* @param {Response} resp
|
||||
* The response to send back to the client.
|
||||
*/
|
||||
Dispatcher.prototype.sendRaw = function(dest, payload) {
|
||||
if (logger.level <= Log.Level.Debug) {
|
||||
logger.debug(this.id + " " + dest + " <- " + JSON.stringify(payload));
|
||||
}
|
||||
Dispatcher.prototype.sendToClient = function(resp) {
|
||||
this.driver.responseCompleted();
|
||||
this.sendMessage(resp);
|
||||
};
|
||||
|
||||
/**
|
||||
* Marshal message to the Marionette message format and send it.
|
||||
*
|
||||
* @param {Command,Response} msg
|
||||
* The message to send.
|
||||
*/
|
||||
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) {
|
||||
this.conn.send(payload);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
Dispatcher.prototype.log_ = function(msg) {
|
||||
if (logger.level > Log.Level.Debug) {
|
||||
return;
|
||||
}
|
||||
let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- ");
|
||||
logger.debug(this.connId + a + msg);
|
||||
};
|
||||
|
|
|
@ -24,7 +24,6 @@ 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");
|
||||
|
@ -95,12 +94,17 @@ 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, emulator) {
|
||||
this.GeckoDriver = function(appName, device, stopSignal, 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
|
||||
|
@ -164,6 +168,7 @@ this.GeckoDriver = function(appName, device, 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;
|
||||
|
@ -1083,11 +1088,6 @@ 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,20 +1133,11 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
|
|||
}
|
||||
|
||||
this.sandboxes[sandboxName].command_id = cmd.id;
|
||||
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.sandboxes[sandboxName].runEmulatorCmd =
|
||||
(cmd, cb) => this.emulator.command(cmd, cb, chromeAsyncError);
|
||||
this.sandboxes[sandboxName].runEmulatorShell =
|
||||
(args, cb) => this.emulator.shell(args, cb, chromeAsyncError);
|
||||
|
||||
this.applyArgumentsToSandbox(win, this.sandboxes[sandboxName], args);
|
||||
|
||||
// NB: win.onerror is not hooked by default due to the inability to
|
||||
|
@ -2800,6 +2791,27 @@ 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.
|
||||
|
@ -2835,11 +2847,6 @@ 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
|
||||
|
@ -3022,7 +3029,8 @@ GeckoDriver.prototype.commands = {
|
|||
"dismissDialog": GeckoDriver.prototype.dismissDialog,
|
||||
"acceptDialog": GeckoDriver.prototype.acceptDialog,
|
||||
"getTextFromDialog": GeckoDriver.prototype.getTextFromDialog,
|
||||
"sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog
|
||||
"sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog,
|
||||
"quitApplication": GeckoDriver.prototype.quitApplication,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,18 +4,14 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
var {classes: Cc, interfaces: Ci} = Components;
|
||||
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
this.EXPORTED_SYMBOLS = ["emulator", "Emulator", "EmulatorCallback"];
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
this.emulator = {};
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
/**
|
||||
* Determines if command ID is an emulator callback.
|
||||
*/
|
||||
this.emulator.isCallback = function(cmdId) {
|
||||
return cmdId < 0;
|
||||
};
|
||||
const logger = Log.repository.getLogger("Marionette");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Emulator"];
|
||||
|
||||
/**
|
||||
* Represents the connection between Marionette and the emulator it's
|
||||
|
@ -26,97 +22,90 @@ this.emulator.isCallback = function(cmdId) {
|
|||
* which is stored in cbs. They are later retreived by their unique ID
|
||||
* using popCallback.
|
||||
*
|
||||
* @param {function(Object)} sendFn
|
||||
* @param {function(Object)} sendToEmulatorFn
|
||||
* 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(sendFn) {
|
||||
this.send = sendFn;
|
||||
this.cbs = [];
|
||||
this.Emulator = function(sendToEmulatorFn) {
|
||||
this.sendToEmulator = sendToEmulatorFn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pops a callback off the stack if found. Otherwise this is a no-op.
|
||||
* Instruct the client to run an Android emulator command.
|
||||
*
|
||||
* @param {number} id
|
||||
* Unique ID associated with the callback.
|
||||
*
|
||||
* @return {?function(Object)}
|
||||
* Callback function that takes an emulator response message as
|
||||
* an argument.
|
||||
* @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.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.command = function(cmd, resCb, errCb) {
|
||||
assertDefined(cmd, "runEmulatorCmd");
|
||||
this.sendToEmulator(
|
||||
"runEmulatorCmd", {emulator_cmd: cmd}, resCb, errCb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pushes callback on to the stack.
|
||||
* Instruct the client to execute Android emulator shell arguments.
|
||||
*
|
||||
* @param {function(Object)} cb
|
||||
* Callback function that takes an emulator response message as
|
||||
* an argument.
|
||||
* @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.
|
||||
*/
|
||||
Emulator.prototype.pushCallback = function(cb) {
|
||||
cb.send_ = this.sendFn;
|
||||
this.cbs.push(cb);
|
||||
Emulator.prototype.shell = function(args, resCb, errCb) {
|
||||
assertDefined(args, "runEmulatorShell");
|
||||
this.sendToEmulator(
|
||||
"runEmulatorShell", {emulator_shell: args}, resCb, errCb);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.processMessage = function(msg) {
|
||||
let resCb = this.resultCallback(msg.json.id);
|
||||
let errCb = this.errorCallback(msg.json.id);
|
||||
|
||||
EmulatorCallback.prototype.command = function(cmd, cb) {
|
||||
this.onresult = cb;
|
||||
this.send_({emulator_cmd: cmd, id: this.id});
|
||||
};
|
||||
switch (msg.name) {
|
||||
case "Marionette:runEmulatorCmd":
|
||||
this.command(msg.json.command, resCb, errCb);
|
||||
break;
|
||||
|
||||
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");
|
||||
case "Marionette:runEmulatorShell":
|
||||
this.shell(msg.json.arguments, resCb, errCb);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Emulator.prototype.resultCallback = function(msgId) {
|
||||
return res => this.sendResult({result: res, id: msgId});
|
||||
};
|
||||
|
||||
Emulator.prototype.errorCallback = function(msgId) {
|
||||
return err => this.sendResult({error: err, id: msgId});
|
||||
};
|
||||
|
||||
Emulator.prototype.sendResult = function(msg) {
|
||||
// sendToListener set explicitly in GeckoDriver's ctor
|
||||
this.sendToListener("emulatorCmdResult", msg);
|
||||
};
|
||||
|
||||
/** Receives IPC messages from the listener. */
|
||||
Emulator.prototype.receiveMessage = function(msg) {
|
||||
try {
|
||||
if (!this.onresult) {
|
||||
return;
|
||||
}
|
||||
this.onresult(msg.result);
|
||||
this.processMessage(msg);
|
||||
} catch (e) {
|
||||
if (this.onerror) {
|
||||
this.onerror(e);
|
||||
}
|
||||
this.sendResult({error: `${e.name}: ${e.message}`, id: msg.json.id});
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,6 +109,24 @@ 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
|
||||
|
@ -216,7 +234,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) {
|
||||
|
|
|
@ -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);
|
||||
mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server);
|
||||
mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
|
||||
mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
|
||||
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);
|
||||
mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server);
|
||||
mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
|
||||
mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
|
||||
mm.removeWeakMessageListener("Marionette:switchedToFrame", this.server);
|
||||
mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.server);
|
||||
mm.removeWeakMessageListener("Marionette:listenersAttached", this.server);
|
||||
|
|
|
@ -16,7 +16,7 @@ marionette.jar:
|
|||
content/EventUtils.js (EventUtils.js)
|
||||
content/ChromeUtils.js (ChromeUtils.js)
|
||||
content/error.js (error.js)
|
||||
content/command.js (command.js)
|
||||
content/message.js (message.js)
|
||||
content/dispatcher.js (dispatcher.js)
|
||||
content/emulator.js (emulator.js)
|
||||
content/modal.js (modal.js)
|
||||
|
|
|
@ -1903,40 +1903,48 @@ 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[_emu_cb_id] = callback;
|
||||
_emu_cbs[asyncTestCommandId] = callback;
|
||||
}
|
||||
sendAsyncMessage("Marionette:runEmulatorCmd", {emulator_cmd: cmd, id: _emu_cb_id});
|
||||
_emu_cb_id += 1;
|
||||
sendAsyncMessage("Marionette:runEmulatorCmd",
|
||||
{command: cmd, id: asyncTestCommandId});
|
||||
}
|
||||
|
||||
function runEmulatorShell(args, callback) {
|
||||
if (callback) {
|
||||
_emu_cbs[_emu_cb_id] = callback;
|
||||
_emu_cbs[asyncTestCommandId] = callback;
|
||||
}
|
||||
sendAsyncMessage("Marionette:runEmulatorShell", {emulator_shell: args, id: _emu_cb_id});
|
||||
_emu_cb_id += 1;
|
||||
sendAsyncMessage("Marionette:runEmulatorShell",
|
||||
{arguments: args, id: asyncTestCommandId});
|
||||
}
|
||||
|
||||
function emulatorCmdResult(msg) {
|
||||
let message = msg.json;
|
||||
let {error, result, id} = msg.json;
|
||||
|
||||
if (error) {
|
||||
let err = new JavaScriptError(error);
|
||||
sendError(err, id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sandboxes[sandboxName]) {
|
||||
return;
|
||||
}
|
||||
let cb = _emu_cbs[message.id];
|
||||
delete _emu_cbs[message.id];
|
||||
let cb = _emu_cbs[id];
|
||||
delete _emu_cbs[id];
|
||||
if (!cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cb(message.result);
|
||||
cb(result);
|
||||
} catch (e) {
|
||||
sendError(e, -1);
|
||||
return;
|
||||
let err = new JavaScriptError(e);
|
||||
sendError(err, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
/* 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;
|
|
@ -93,7 +93,8 @@ MarionetteServer.prototype.driverFactory = function(emulator) {
|
|||
Services.io.offline = false;
|
||||
}
|
||||
|
||||
return new GeckoDriver(appName, device, emulator);
|
||||
let stopSignal = () => this.stop();
|
||||
return new GeckoDriver(appName, device, stopSignal, emulator);
|
||||
};
|
||||
|
||||
MarionetteServer.prototype.start = function() {
|
||||
|
@ -129,20 +130,17 @@ MarionetteServer.prototype.onSocketAccepted = function(
|
|||
let transport = new DebuggerTransport(input, output);
|
||||
let connId = "conn" + this.nextConnId++;
|
||||
|
||||
let stopSignal = () => this.stop();
|
||||
let dispatcher = new Dispatcher(connId, transport, this.driverFactory, stopSignal);
|
||||
let dispatcher = new Dispatcher(connId, transport, this.driverFactory.bind(this));
|
||||
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.id;
|
||||
let id = conn.connId;
|
||||
delete this.conns[id];
|
||||
logger.info(`Closed connection ${id}`);
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче