gecko-dev/testing/marionette/dispatcher.js

289 lines
8.4 KiB
JavaScript
Raw Normal View History

/* 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.
* @param {function()} stopSignal
* Signal to stop the Marionette server.
*/
this.Dispatcher = function(connId, transport, driverFactory, stopSignal) {
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);
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) {
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.stopSignal_();
this.sendOk(id);
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.driver.responseCompleted();
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
};