зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1107706: Part 4: Add dispatching mechanism to encapsulate connection
The dispatcher is analogous to the client socket connection, and handles receiving packets and closing connections. It also encompasses some of the functionality needed to establish the devtools and Marionette connection, that previously used to live in MarionetteServerConnection in marionette-server.js. For each connection, recognised commands will be forwarded to the command processor (command.js) unless a handler is defined in Dispatcher.requests. --HG-- extra : rebase_source : a86a768323f9cf9450bc17a3b105265440e2c861 extra : source : 7abef4010b3094d3f276fc16cfbae43b55da7b0d
This commit is contained in:
Родитель
a9889b9f31
Коммит
2db8cf1de0
|
@ -0,0 +1,280 @@
|
|||
/* 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";
|
||||
|
||||
const {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/command.js");
|
||||
Cu.import("chrome://marionette/content/emulator.js");
|
||||
Cu.import("chrome://marionette/content/error.js");
|
||||
Cu.import("chrome://marionette/content/driver.js");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Dispatcher"];
|
||||
|
||||
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
|
||||
* their correct destinations.
|
||||
*
|
||||
* @param {number} connId
|
||||
* Unique identifier of the connection this dispatcher should handle.
|
||||
* @param {DebuggerTransport} transport
|
||||
* Debugger transport connection to the client.
|
||||
* @param {function(Emulator): GeckoDriver} driverFactory
|
||||
* A factory function that takes an Emulator as argument and produces
|
||||
* a GeckoDriver.
|
||||
*/
|
||||
this.Dispatcher = function(connId, transport, driverFactory) {
|
||||
this.id = connId;
|
||||
this.conn = transport;
|
||||
|
||||
// Marionette uses a protocol based on the debugger server, which
|
||||
// requires passing back actor ID's with responses. Unlike the debugger
|
||||
// server, we don't actually have multiple actors, so just use a dummy
|
||||
// value of "0".
|
||||
this.actorId = "0";
|
||||
|
||||
// callback for when connection is closed
|
||||
this.onclose = null;
|
||||
|
||||
// transport hooks are Dispatcher.prototype.onPacket
|
||||
// and Dispatcher.prototype.onClosed
|
||||
this.conn.hooks = this;
|
||||
|
||||
this.emulator = new Emulator(msg => this.sendResponse(msg, -1));
|
||||
this.driver = driverFactory(this.emulator);
|
||||
this.commandProcessor = new CommandProcessor(this.driver);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
logger.debug(`${this.id} -> ${packet.toSource()}`);
|
||||
|
||||
if (this.requests && this.requests[packet.name]) {
|
||||
this.requests[packet.name].bind(this)(packet);
|
||||
} else {
|
||||
let id = this.beginNewCommand();
|
||||
let ok = this.sendOk.bind(this);
|
||||
let send = this.send.bind(this);
|
||||
this.commandProcessor.execute(packet, ok, send, id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Debugger transport callback that cleans up
|
||||
* after a connection is closed.
|
||||
*/
|
||||
Dispatcher.prototype.onClosed = function(status) {
|
||||
this.driver.sessionTearDown();
|
||||
if (this.onclose) {
|
||||
this.onclose(this);
|
||||
}
|
||||
};
|
||||
|
||||
// Dispatcher specific command handlers:
|
||||
|
||||
Dispatcher.prototype.getMarionetteID = function() {
|
||||
let id = this.beginNewCommand();
|
||||
this.sendResponse({from: "root", id: this.actorId}, id);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Quits Firefox with the provided flags and tears down the current
|
||||
* session.
|
||||
*/
|
||||
Dispatcher.prototype.quitApplication = function(msg) {
|
||||
let id = this.beginNewCommand();
|
||||
|
||||
if (this.driver.appName != "Firefox") {
|
||||
this.sendError({
|
||||
"message": "In app initiated quit only supported on Firefox",
|
||||
"status": 500
|
||||
}, id);
|
||||
return;
|
||||
}
|
||||
|
||||
let flags = Ci.nsIAppStartup.eAttemptQuit;
|
||||
for (let k of msg.parameters.flags) {
|
||||
flags |= Ci.nsIAppStartup[k];
|
||||
}
|
||||
|
||||
this.driver.sessionTearDown();
|
||||
Services.startup.quit(flags);
|
||||
};
|
||||
|
||||
// Convenience methods:
|
||||
|
||||
Dispatcher.prototype.sayHello = function() {
|
||||
let id = this.beginNewCommand();
|
||||
let yo = {from: "root", applicationType: "gecko", traits: []};
|
||||
this.sendResponse(yo, id);
|
||||
};
|
||||
|
||||
Dispatcher.prototype.sendOk = function(cmdId) {
|
||||
this.sendResponse({from: this.actorId, ok: true}, cmdId);
|
||||
};
|
||||
|
||||
Dispatcher.prototype.sendError = function(err, cmdId) {
|
||||
let packet = {
|
||||
from: this.actorId,
|
||||
status: err.status,
|
||||
sessionId: this.driver.sessionId,
|
||||
error: err
|
||||
};
|
||||
this.sendResponse(packet, cmdId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Marshals and sends message to either client or emulator based on the
|
||||
* provided {@code cmdId}.
|
||||
*
|
||||
* This routine produces a Marionette protocol packet, which is different
|
||||
* to a WebDriver protocol response in that it contains an extra key
|
||||
* {@code from} for the debugger transport actor ID. It also replaces the
|
||||
* key {@code value} with {@code error} when {@code msg.status} isn't
|
||||
* {@code 0}.
|
||||
*
|
||||
* @param {Object} msg
|
||||
* Object with the properties {@code value}, {@code status}, and
|
||||
* {@code sessionId}.
|
||||
* @param {UUID} cmdId
|
||||
* The unique identifier for the command the message is a response to.
|
||||
*/
|
||||
Dispatcher.prototype.send = function(msg, cmdId) {
|
||||
let packet = {
|
||||
from: this.actorId,
|
||||
value: msg.value,
|
||||
status: msg.status,
|
||||
sessionId: msg.sessionId,
|
||||
};
|
||||
|
||||
if (typeof packet.value == "undefined") {
|
||||
packet.value = null;
|
||||
}
|
||||
|
||||
// the Marionette protocol sends errors using the "error"
|
||||
// key instead of, as Selenium, "value"
|
||||
if (!error.isSuccess(msg.status)) {
|
||||
packet.error = packet.value;
|
||||
delete packet.value;
|
||||
}
|
||||
|
||||
this.sendResponse(packet, cmdId);
|
||||
};
|
||||
|
||||
// Low-level methods:
|
||||
|
||||
/**
|
||||
* Delegates message to client or emulator based on the provided
|
||||
* {@code cmdId}. The message is sent over the debugger transport socket.
|
||||
*
|
||||
* The command ID is a unique identifier assigned to the client's request
|
||||
* that is used to distinguish the asynchronous responses.
|
||||
*
|
||||
* Whilst responses to commands are synchronous and must be sent in the
|
||||
* 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.
|
||||
*/
|
||||
Dispatcher.prototype.sendResponse = function(payload, cmdId) {
|
||||
if (emulator.isCallback(cmdId)) {
|
||||
this.sendToEmulator(payload);
|
||||
} else {
|
||||
this.sendToClient(payload, cmdId);
|
||||
this.commandId = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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(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.sendRaw("client", payload);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends payload as-is over debugger transport socket to client,
|
||||
* and logs it.
|
||||
*/
|
||||
Dispatcher.prototype.sendRaw = function(dest, payload) {
|
||||
logger.debug(`${this.id} ${dest} <- ${payload.toSource()}`);
|
||||
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 = {
|
||||
getMarionetteID: Dispatcher.prototype.getMarionetteID,
|
||||
emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult,
|
||||
quitApplication: Dispatcher.prototype.quitApplication
|
||||
};
|
|
@ -15,7 +15,6 @@ marionette.jar:
|
|||
content/EventUtils.js (EventUtils.js)
|
||||
content/ChromeUtils.js (ChromeUtils.js)
|
||||
content/error.js (error.js)
|
||||
content/cmdproc.js (cmdproc.js)
|
||||
content/command.js (command.js)
|
||||
content/dispatcher.js (dispatcher.js)
|
||||
content/emulator.js (emulator.js)
|
||||
|
|
Загрузка…
Ссылка в новой задаче