gecko/testing/marionette/dispatcher.js

239 lines
6.8 KiB
JavaScript

/* 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 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
* 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;
// 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.send(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) {
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(status) {
this.driver.sessionTearDown();
if (this.onclose) {
this.onclose(this);
}
};
// Dispatcher specific command handlers:
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(new WebDriverError("In app initiated quit only supported in Firefox"));
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 whatHo = {
applicationType: "gecko",
marionetteProtocol: PROTOCOL_VERSION,
};
this.send(whatHo, id);
};
Dispatcher.prototype.sendOk = function(cmdId) {
this.send({}, cmdId);
};
Dispatcher.prototype.sendError = function(err, cmdId) {
let resp = new Response(cmdId, this.send.bind(this));
resp.sendError(err);
};
/**
* 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.send = function(payload, cmdId) {
if (emulator.isCallback(cmdId)) {
this.sendToEmulator(payload);
} else {
this.sendToClient(payload, cmdId);
this.commandId = null;
}
};
// 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.
*
* 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) {
if (logger.level <= Log.Level.Debug) {
logger.debug(this.id + " " + dest + " <- " + JSON.stringify(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
};