2015-03-18 12:27:29 +00:00
|
|
|
/* 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.
|
2015-03-20 20:44:17 +00:00
|
|
|
* @param {function()} stopSignal
|
|
|
|
* Signal to stop the Marionette server.
|
2015-03-18 12:27:29 +00:00
|
|
|
*/
|
2015-03-20 20:44:17 +00:00
|
|
|
this.Dispatcher = function(connId, transport, driverFactory, stopSignal) {
|
2015-03-18 12:27:29 +00:00
|
|
|
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);
|
2015-03-20 20:44:17 +00:00
|
|
|
|
|
|
|
this.stopSignal_ = stopSignal;
|
2015-03-18 12:27:29 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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];
|
|
|
|
}
|
|
|
|
|
2015-03-20 20:44:17 +00:00
|
|
|
this.stopSignal_();
|
|
|
|
this.sendOk(id);
|
|
|
|
|
2015-03-18 12:27:29 +00:00
|
|
|
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;
|
|
|
|
}
|
2015-03-19 21:12:58 +00:00
|
|
|
this.driver.responseCompleted();
|
2015-03-18 12:27:29 +00:00
|
|
|
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
|
|
|
|
};
|