/* 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 EXPORTED_SYMBOLS = ["Command", "Message", "Response"]; const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" ); XPCOMUtils.defineLazyModuleGetters(this, { assert: "chrome://marionette/content/assert.js", error: "chrome://marionette/content/error.js", truncate: "chrome://marionette/content/format.js", }); /** Representation of the packets transproted over the wire. */ class Message { /** * @param {number} messageID * Message ID unique identifying this message. */ constructor(messageID) { this.id = assert.integer(messageID); } toString() { let content = JSON.stringify(this.toPacket()); return truncate`${content}`; } /** * Converts a data packet into a {@link Command} or {@link Response}. * * @param {Array.} data * A four element array where the elements, in sequence, signifies * message type, message ID, method name or error, and parameters * or result. * * @return {Message} * Based on the message type, a {@link Command} or {@link Response} * instance. * * @throws {TypeError} * If the message type is not recognised. */ static fromPacket(data) { const [type] = data; switch (type) { case Command.Type: return Command.fromPacket(data); case Response.Type: return Response.fromPacket(data); default: throw new TypeError( "Unrecognised message type in packet: " + JSON.stringify(data) ); } } } /** * Messages may originate from either the server or the client. * Because the remote protocol is full duplex, both endpoints may be * the origin of both commands and responses. * * @enum * @see {@link Message} */ Message.Origin = { /** Indicates that the message originates from the client. */ Client: 0, /** Indicates that the message originates from the server. */ Server: 1, }; /** * 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 {@link fromPacket} function. The format of * a message is: * *
 *     [type, id, name, params]
 * 
* * where * *
*
type (integer) *
* Must be zero (integer). Zero means that this message is * a command. * *
id (integer) *
* Integer used as a sequence number. The server replies with * the same ID for the response. * *
name (string) *
* String representing the command name with an associated set * of remote end steps. * *
params (JSON Object or null) *
* 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 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 * function onerror({Object}), * function onresult({Object}), and * function onresult({Response}): * * @param {number} messageID * Message ID unique identifying this message. * @param {string} name * Command name. * @param {Object.} params * Command parameters. */ class Command extends Message { constructor(messageID, name, params = {}) { super(messageID); this.name = assert.string(name); this.parameters = assert.object(params); this.onerror = null; this.onresult = null; this.origin = Message.Origin.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 * onerror or onresult handlers to. */ onresponse(resp) { if (this.onerror && resp.error) { this.onerror(resp.error); } else if (this.onresult && resp.body) { this.onresult(resp.body); } } /** * Encodes the command to a packet. * * @return {Array} * Packet. */ toPacket() { return [Command.Type, this.id, this.name, this.parameters]; } /** * Converts a data packet into {@link Command}. * * @param {Array.} data * A four element array where the elements, in sequence, signifies * message type, message ID, command name, and parameters. * * @return {Command} * Representation of packet. * * @throws {TypeError} * If the message type is not recognised. */ static fromPacket(payload) { let [type, msgID, name, params] = payload; assert.that(n => n === Command.Type)(type); // if parameters are given but null, treat them as undefined if (params === null) { params = undefined; } return new Command(msgID, name, params); } } Command.Type = 0; /** * @callback ResponseCallback * * @param {Response} resp * Response to handle. */ /** * 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 * {@link server.TCPConnection#execute when a command has finished * executing, and any modifications made subsequent to that will have * no effect. * * @param {number} messageID * Message ID tied to the corresponding command request this is * a response for. * @param {ResponseHandler} respHandler * Function callback called on sending the response. */ class Response extends Message { constructor(messageID, respHandler = () => {}) { super(messageID); this.respHandler_ = assert.callable(respHandler); this.error = null; this.body = { value: null }; this.origin = Message.Origin.Server; this.sent = false; } /** * 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 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 err is not a {@link WebDriverError}, the error * is propagated, i.e. rethrown. */ sendError(err) { this.error = error.wrap(err).toJSON(); this.body = null; this.send(); // propagate errors which are implementation problems if (!error.isWebDriverError(err)) { throw err; } } /** * Encodes the response to a packet. * * @return {Array} * Packet. */ toPacket() { return [Response.Type, this.id, this.error, this.body]; } /** * Converts a data packet into {@link Response}. * * @param {Array.} data * A four element array where the elements, in sequence, signifies * message type, message ID, error, and result. * * @return {Response} * Representation of packet. * * @throws {TypeError} * If the message type is not recognised. */ static fromPacket(payload) { let [type, msgID, err, body] = payload; assert.that(n => n === Response.Type)(type); let resp = new Response(msgID); resp.error = assert.string(err); resp.body = body; return resp; } } Response.Type = 1; this.Message = Message; this.Command = Command; this.Response = Response;